From 069d63a10c0958a02ba3a825d37df6e20fa5fe88 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Tue, 11 Apr 2023 12:23:16 +0200 Subject: [PATCH 001/115] Fixed height detection when using seq. printing (broken in 3349644, fixes #10312) --- src/libslic3r/PrintObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0b0610647..5ee3a604d 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -102,7 +102,7 @@ PrintObject::PrintObject(Print* print, ModelObject* model_object, const Transfor m_center_offset = Point::new_scale(bbox_center.x(), bbox_center.y()); // Size of the transformed mesh. This bounding may not be snug in XY plane, but it is snug in Z. m_size = (bbox.size() * (1. / SCALING_FACTOR)).cast(); - m_size.z() = model_object->max_z(); + m_size.z() = coord_t(model_object->max_z() * (1. / SCALING_FACTOR)); this->set_instances(std::move(instances)); } From f4b935b66197d20f014c15f20fb412247280a44d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 11 Apr 2023 13:14:23 +0200 Subject: [PATCH 002/115] SPE-1449 - Fixed object disappearing when opening Hollow or SLA support gizmos on LINUX --- src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp index 348fa5ca7..2e76ebc96 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp @@ -135,7 +135,11 @@ void GLGizmoSlaBase::render_volumes() const Camera& camera = wxGetApp().plater()->get_camera(); ClippingPlane clipping_plane = (m_c->object_clipper()->get_position() == 0.0) ? ClippingPlane::ClipsNothing() : *m_c->object_clipper()->get_clipping_plane(); - clipping_plane.set_normal(-clipping_plane.get_normal()); + if (m_c->object_clipper()->get_position() != 0.0) + clipping_plane.set_normal(-clipping_plane.get_normal()); + else + // on Linux the clipping plane does not work when using DBL_MAX + clipping_plane.set_offset(FLT_MAX); m_volumes.set_clipping_plane(clipping_plane.get_data()); for (GLVolume* v : m_volumes.volumes) { From d3d48e98955303181cd228729fd88a4cc0cedf50 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 12 Apr 2023 16:17:28 +0200 Subject: [PATCH 003/115] Fixed long standing bug in elephant foot compensation of holes. --- src/libslic3r/ClipperUtils.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index ed76fc66a..9dfee3813 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -1317,8 +1317,12 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector 0.); From bceed00ae8a13609ca02364bc5158c3c5b5ede85 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 12 Apr 2023 17:02:52 +0200 Subject: [PATCH 004/115] Fix of Layer::build_up_down_graph() for non-manifold inputs by shrinking the input expolygons before intersecting them. GH #10150, #10158, SPE-1621, SPE-1612 --- src/libslic3r/Layer.cpp | 79 +++++++++++++++++++++++++++++++---------- 1 file changed, 60 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 2d1132283..7802fe983 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -60,7 +60,10 @@ void Layer::make_slices() } // used by Layer::build_up_down_graph() -[[nodiscard]] static ClipperLib_Z::Paths expolygons_to_zpaths(const ExPolygons &expolygons, coord_t isrc) +// Shrink source polygons one by one, so that they will be separated if they were touching +// at vertices (non-manifold situation). +// Then convert them to Z-paths with Z coordinate indicating index of the source expolygon. +[[nodiscard]] static ClipperLib_Z::Paths expolygons_to_zpaths_shrunk(const ExPolygons &expolygons, coord_t isrc) { size_t num_paths = 0; for (const ExPolygon &expolygon : expolygons) @@ -69,14 +72,49 @@ void Layer::make_slices() ClipperLib_Z::Paths out; out.reserve(num_paths); - for (const ExPolygon &expolygon : expolygons) { - for (size_t icontour = 0; icontour < expolygon.num_contours(); ++ icontour) { - const Polygon &contour = expolygon.contour_or_hole(icontour); - out.emplace_back(); - ClipperLib_Z::Path &path = out.back(); - path.reserve(contour.size()); - for (const Point &p : contour.points) - path.push_back({ p.x(), p.y(), isrc }); + ClipperLib::Paths contours; + ClipperLib::Paths holes; + ClipperLib::Clipper clipper; + ClipperLib::ClipperOffset co; + ClipperLib::Paths out2; + + static constexpr const float delta = ClipperSafetyOffset; // *10.f; + co.MiterLimit = scaled(3.); +// Use the default zero edge merging distance. For this kind of safety offset the accuracy of normal direction is not important. +// co.ShortestEdgeLength = delta * ClipperOffsetShortestEdgeFactor; + + for (const ExPolygon &expoly : expolygons) { + contours.clear(); + co.Clear(); + co.AddPath(expoly.contour.points, ClipperLib::jtMiter, ClipperLib::etClosedPolygon); + co.Execute(contours, - delta); + if (! contours.empty()) { + holes.clear(); + for (const Polygon &hole : expoly.holes) { + co.Clear(); + co.AddPath(hole.points, ClipperLib::jtMiter, ClipperLib::etClosedPolygon); + // Execute reorients the contours so that the outer most contour has a positive area. Thus the output + // contours will be CCW oriented even though the input paths are CW oriented. + // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. + out2.clear(); + co.Execute(out2, delta); + append(holes, std::move(out2)); + } + // Subtract holes from the contours. + if (! holes.empty()) { + clipper.Clear(); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + contours.clear(); + clipper.Execute(ClipperLib::ctDifference, contours, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + } + for (const auto &contour : contours) { + out.emplace_back(); + ClipperLib_Z::Path &path = out.back(); + path.reserve(contour.size()); + for (const Point &p : contour) + path.push_back({ p.x(), p.y(), isrc }); + } } ++ isrc; } @@ -183,6 +221,10 @@ static void connect_layer_slices( break; } } + //FIXME remove the following block one day, it should not be needed. + // The following shall not happen now as the source expolygons are being shrunk a bit before intersecting, + // thus each point of each intersection polygon should fit completely inside one of the original (unshrunk) expolygons. + assert(found); if (!found) { // The check above might sometimes fail when the polygons overlap only on points, which causes the clipper to detect no intersection. // The problem happens rarely, mostly on simple polygons (in terms of number of points), but regardless of size! @@ -191,10 +233,7 @@ static void connect_layer_slices( // layer B = Polygon{(-24877897,-11100524),(-22504249,-8726874),(-22504249,11477151),(-23244827,12218916),(-23752371,12727276),(-25002495,12727276),(-27502745,10227026),(-27502745,-12727274),(-26504645,-12727274)} // note that first point is not identical, and the check above picks (-24877897,-11100524) as the first contour point (polynode.Contour.front()). // that point is sadly slightly outisde of the layer A, so no link is detected, eventhough they are overlaping "completely" - Polygon contour_poly; - for (const auto& p : polynode.Contour) { - contour_poly.points.emplace_back(p.x(), p.y()); - } + Polygon contour_poly(ClipperZUtils::from_zpath(polynode.Contour)); BoundingBox contour_aabb{contour_poly.points}; for (int l = int(m_above.lslices_ex.size()) - 1; l >= 0; --l) { LayerSlice &lslice = m_above.lslices_ex[l]; @@ -222,11 +261,11 @@ static void connect_layer_slices( break; } } + //FIXME remove the following block one day, it should not be needed. + // The following shall not happen now as the source expolygons are being shrunk a bit before intersecting, + // thus each point of each intersection polygon should fit completely inside one of the original (unshrunk) expolygons. if (!found) { // Explanation for aditional check is above. - Polygon contour_poly; - for (const auto &p : polynode.Contour) { - contour_poly.points.emplace_back(p.x(), p.y()); - } + Polygon contour_poly(ClipperZUtils::from_zpath(polynode.Contour)); BoundingBox contour_aabb{contour_poly.points}; for (int l = int(m_below.lslices_ex.size()) - 1; l >= 0; --l) { LayerSlice &lslice = m_below.lslices_ex[l]; @@ -249,6 +288,8 @@ static void connect_layer_slices( found = true; } if (found) { + assert(i >= 0 && i < m_below.lslices_ex.size()); + assert(j >= 0 && j < m_above.lslices_ex.size()); // Subtract area of holes from the area of outer contour. double area = ClipperLib_Z::Area(polynode.Contour); for (int icontour = 0; icontour < polynode.ChildCount(); ++ icontour) @@ -340,9 +381,9 @@ static void connect_layer_slices( void Layer::build_up_down_graph(Layer& below, Layer& above) { coord_t paths_below_offset = 0; - ClipperLib_Z::Paths paths_below = expolygons_to_zpaths(below.lslices, paths_below_offset); + ClipperLib_Z::Paths paths_below = expolygons_to_zpaths_shrunk(below.lslices, paths_below_offset); coord_t paths_above_offset = paths_below_offset + coord_t(below.lslices.size()); - ClipperLib_Z::Paths paths_above = expolygons_to_zpaths(above.lslices, paths_above_offset); + ClipperLib_Z::Paths paths_above = expolygons_to_zpaths_shrunk(above.lslices, paths_above_offset); #ifndef NDEBUG coord_t paths_end = paths_above_offset + coord_t(above.lslices.size()); #endif // NDEBUG From fd3c41b4d35201fdbd3d6c223b4342bc93525beb Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 12 Apr 2023 17:38:09 +0200 Subject: [PATCH 005/115] Fix of #10257 SPE-1641 The object labeling likely never worked. Likely it was contributed, but not reviewed sufficiently (by me I suppose). Now the object ID is calculated as an index in the list of PrintObjects, the order is arbitrary but stable, indices start with 0 and incremented for every printed object with no gap in indices. We are not quite sure how the indices are used by the OctoPrint "Cancel Object" plugin, I suppose this change is sufficient. --- src/libslic3r/GCode.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index b0fdcc015..8d0c7f070 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2354,11 +2354,10 @@ void GCode::process_layer_single_object( // Round 1 (wiping into object or infill) or round 2 (normal extrusions). const bool print_wipe_extrusions) { - //FIXME what the heck ID is this? Layer ID or Object ID? More likely an Object ID. - uint32_t layer_id = 0; - bool first = true; + bool first = true; + int object_id = 0; // Delay layer initialization as many layers may not print with all extruders. - auto init_layer_delayed = [this, &print_instance, &layer_to_print, layer_id, &first, &gcode]() { + auto init_layer_delayed = [this, &print_instance, &layer_to_print, &first, &object_id, &gcode]() { if (first) { first = false; const PrintObject &print_object = print_instance.print_object; @@ -2374,8 +2373,14 @@ void GCode::process_layer_single_object( m_avoid_crossing_perimeters.use_external_mp_once(); m_last_obj_copy = this_object_copy; this->set_origin(unscale(offset)); - if (this->config().gcode_label_objects) - gcode += std::string("; printing object ") + print_object.model_object()->name + " id:" + std::to_string(layer_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; + if (this->config().gcode_label_objects) { + for (const PrintObject *po : print_object.print()->objects()) + if (po == &print_object) + break; + else + ++ object_id; + gcode += std::string("; printing object ") + print_object.model_object()->name + " id:" + std::to_string(object_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; + } } }; @@ -2548,7 +2553,7 @@ void GCode::process_layer_single_object( } } if (! first && this->config().gcode_label_objects) - gcode += std::string("; stop printing object ") + print_object.model_object()->name + " id:" + std::to_string(layer_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; + gcode += std::string("; stop printing object ") + print_object.model_object()->name + " id:" + std::to_string(object_id) + " copy " + std::to_string(print_instance.instance_id) + "\n"; } void GCode::apply_print_config(const PrintConfig &print_config) From a092fdc1ed1d175a1e805a12abbcfa0841f9db23 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 13 Apr 2023 10:06:14 +0200 Subject: [PATCH 006/115] Revert "Fixed long standing bug in elephant foot compensation of holes." This reverts commit d3d48e98955303181cd228729fd88a4cc0cedf50. --- src/libslic3r/ClipperUtils.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 9dfee3813..ed76fc66a 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -1317,12 +1317,8 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector 0.); From 5bb7428aa412317ab3f06cee2a73e2a7926435bd Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 13 Apr 2023 13:09:32 +0200 Subject: [PATCH 007/115] SPE-1655 - Fixed adding an instance to a mirrored object --- src/libslic3r/Model.cpp | 9 +++------ src/libslic3r/Model.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 4 +++- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 08e70964e..6c350e7b4 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -837,13 +837,10 @@ ModelInstance* ModelObject::add_instance(const ModelInstance &other) return i; } -ModelInstance* ModelObject::add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror) +ModelInstance* ModelObject::add_instance(const Geometry::Transformation& trafo) { - auto *instance = add_instance(); - instance->set_offset(offset); - instance->set_scaling_factor(scaling_factor); - instance->set_rotation(rotation); - instance->set_mirror(mirror); + ModelInstance* instance = add_instance(); + instance->set_transformation(trafo); return instance; } diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 60746f10b..55aceeeb2 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -392,7 +392,7 @@ public: ModelInstance* add_instance(); ModelInstance* add_instance(const ModelInstance &instance); - ModelInstance* add_instance(const Vec3d &offset, const Vec3d &scaling_factor, const Vec3d &rotation, const Vec3d &mirror); + ModelInstance* add_instance(const Geometry::Transformation& trafo); void delete_instance(size_t idx); void delete_last_instance(); void clear_instances(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c3a2dcda0..9e1638cae 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6070,7 +6070,9 @@ void Plater::increase_instances(size_t num, int obj_idx/* = -1*/) double offset = offset_base; for (size_t i = 0; i < num; i++, offset += offset_base) { Vec3d offset_vec = model_instance->get_offset() + Vec3d(offset, offset, 0.0); - model_object->add_instance(offset_vec, model_instance->get_scaling_factor(), model_instance->get_rotation(), model_instance->get_mirror()); + Geometry::Transformation trafo = model_instance->get_transformation(); + trafo.set_offset(offset_vec); + model_object->add_instance(trafo); // p->print.get_object(obj_idx)->add_copy(Slic3r::to_2d(offset_vec)); } From c1e145b86c6da0fedf257f39c79934d2d80b46d4 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 13 Apr 2023 13:35:31 +0200 Subject: [PATCH 008/115] SPE-1656 - When adding an instance use the orientation of the currently selected instance --- src/slic3r/GUI/Plater.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 9e1638cae..5808b196d 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -1827,6 +1827,7 @@ struct Plater::priv const Selection& get_selection() const; Selection& get_selection(); int get_selected_object_idx() const; + int get_selected_instance_idx() const; int get_selected_volume_idx() const; void selection_changed(); void object_list_changed(); @@ -2967,6 +2968,17 @@ int Plater::priv::get_selected_object_idx() const return (0 <= idx && idx < int(model.objects.size())) ? idx : -1; } +int Plater::priv::get_selected_instance_idx() const +{ + const int obj_idx = get_selected_object_idx(); + if (obj_idx >= 0) { + const int inst_idx = get_selection().get_instance_idx(); + return (0 <= inst_idx && inst_idx < int(model.objects[obj_idx]->instances.size())) ? inst_idx : -1; + } + else + return -1; +} + int Plater::priv::get_selected_volume_idx() const { auto& selection = get_selection(); @@ -6062,7 +6074,8 @@ void Plater::increase_instances(size_t num, int obj_idx/* = -1*/) } ModelObject* model_object = p->model.objects[obj_idx]; - ModelInstance* model_instance = model_object->instances.back(); + const int inst_idx = p->get_selected_instance_idx(); + ModelInstance* model_instance = (inst_idx >= 0) ? model_object->instances[inst_idx] : model_object->instances.back(); bool was_one_instance = model_object->instances.size()==1; From c8468839da8d0ac258261afe417a0d66174b1a8d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 13 Apr 2023 15:34:09 +0200 Subject: [PATCH 009/115] Fixed method ExportLines::update() to avoid potential deferencing of invalid iterator --- src/libslic3r/GCode/GCodeProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index a56c1e132..728d381f1 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3566,7 +3566,7 @@ void GCodeProcessor::post_process() ++m_curr_g1_id; } - if (it != init_it || m_curr_g1_id == 0) + if ((it != m_machine.g1_times_cache.end() && it != init_it) || m_curr_g1_id == 0) m_time = it->elapsed_time; } From dd211b29eb77b0da16fdf02c0bea59f7ec8cec65 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:34:07 +0200 Subject: [PATCH 010/115] Voron Switchwire resources. Based on https://github.com/slic3r/slic3r-profiles/pull/35. --- .../profiles/Voron/Voron_SW_thumbnail.png | Bin 0 -> 32835 bytes .../profiles/Voron/bedtexture-SW-250x210.png | Bin 0 -> 470766 bytes resources/profiles/Voron/printbed-SW-MK52.stl | Bin 0 -> 91884 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/profiles/Voron/Voron_SW_thumbnail.png create mode 100644 resources/profiles/Voron/bedtexture-SW-250x210.png create mode 100644 resources/profiles/Voron/printbed-SW-MK52.stl diff --git a/resources/profiles/Voron/Voron_SW_thumbnail.png b/resources/profiles/Voron/Voron_SW_thumbnail.png new file mode 100644 index 0000000000000000000000000000000000000000..4585c01620bcbec09647cae86a5ea06c868fb20e GIT binary patch literal 32835 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%0A6CP=A|B0DkT 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 KlB^N`6!d>e?*^s- literal 0 HcmV?d00001 diff --git a/resources/profiles/Voron/bedtexture-SW-250x210.png b/resources/profiles/Voron/bedtexture-SW-250x210.png new file mode 100644 index 0000000000000000000000000000000000000000..e027c30ad5f77c1ca30430cc42857891a4fffc17 GIT binary patch literal 470766 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

DCY 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

L8sDw0gy0$bSL&Ts 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�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 }otV;9d#m)gAZl z2!Swd>?IWMl+kD>+N^+nj#-Gm^nNgO0=(Zn=gWPGS|M(beP?2)?Y*0*PCVwco4sC=H$rdVq!17D~j3%hsBIu-n&&vo3R(ZW&HZ84A3nCo{v2f_I=~{G>{@+ z5i)Pka4&4)EB{#T_Vh_N{IE?oE)5xB2XXQ@&D9{_BC@3g+2u_s8cu_vU!|6AQ{1ng zuZHD&(Pm%A8jw1+P*rz2@i+J;oII1oepJpfMD#x2qA*Lzp16pqA;iYk;3LoW$@wxd zMAkD|xJ_SK9;U|C+z?j5T>gaPIh0nV8i&4$iCUEuRHzdX0}XTgV}8G^EsE|#sUU~t zG1WPy-(cOGd#Q*LEZ-I*Jr5Zgyp7>ve6LAsAd#k;rq>R=ImIIawX;jnNo`6!w)>GO zm}HDM7-!0%b>2{fi+@ZYBuiz7Y?uleI_;m0;h)M+op{;y3D18+UO3Ws8#_JWP|m8t zNT-)(yWC>H>~N&072ALf3VK#9J&ty~_#&*plsxMU$DM}!$t&Qh!<(IfUlkHdMj2iwe>p_4-egUoqFl_ zuBdVcto$zhyrt6VRJQVw${dJY2wCQ{=5rC6r*AX0{Al`6v{Mk1hgBW%PQ7@P{{-9c zz<0mu?JJ(p;H^1p(I@&+PH0EYu%$Un_NO`7>)(z!T{C1(Gb|+V6*h|_Av8vR-t${^ zI@=c`l~_I#uaxW)E+AyaunU+V9R@wM^lbd7_}Ng7)AtCCN^2Um(@k)i+DG#Eq1$aq zzUU6|?D!1`rd_EOd>_Ysjqrz_O*Frj61aAHTxH3UN>*rxLfKqAqU2kD?C@vb zOaZnsC}b`_>j2pUzBnRW7-u649r$m`S;|Ne_kA$)cmJ4!5YK^MkMYR-xGrzW^9Nw& z3J!NI`+$i1g7|=tlQUYhc@(~f)0 zd}U4s+W`}czIj#leYOP=l>PuKy}-=7N*e8uV66L-B39>*$gU&Y`Y$~?ZrAJTU!_Nz z8)2vf)-_Qi10+|7(1j~;AfAH6_Ijw*)pr|uG{AA2m3+QUud^<8B>F>9K(Z-MS$jfR{X$WiwrG;R^uwm}Cnav3 zcsFa^R-^TU!Ik3KkBX$=1w|kr`bq5)(Z59%yzPP| zaplO~l_MI8oQuI(hme$FwT%?-r4D^92KuE&_{_7B#u261CiXJ?uarxZ_>dcwsjcNK z{mxZJZIr2`C?mKYb8%Px`19E#WAXI6<3%0#K=u62j>O-pEnv3}g$J+lSdj#X=3B;| z+?9_VmxLJ#&U`FUcf@eprF{r=s_Vj==rx+>jcnL=@>8>SwCu*Eu|s{|2Qs(T&$blJ z6VISor%(DTi6Ln_BAf$JukwCcEVT zJD|vBWs<3?$>4ZdNA;S!frVdF%F?OU1?T%8^RzEI@Q0k6s5?ss27x5R#DcouLmCx2 zNKsuiBG7caWcG56HmqLG5|hX6N^641@-gN;Y5OkaWJ*F+nt11TB?KsW0ABv4xsgxJ zb{&`I(c@EqTAGUKNL4Sv&xiQ+z*W0)_*+Op{=+Z*=S|pvFU3{!uK|O`I#du?f^5mY zIR?^vZ?`b2oK?3US>O2D$ELBejBIHL7m4{Dv1>HbRLRkmL5iE>oST4+@*blx)|h-e z%Ga~_o6t|W3m4>2`i6s-jf7R_q|HbIjE=QqB%BlCAG4=A6|O`Fq7~$WH*Q=&l}^7z z?ni#!t#X|58~H;Xw)BDV_edabkv@9fqkkqs<9Fsb#2yt6eVK6gj9@t;YE=8$^z_KLU2#AAj$A&9&xJ(gff| zXQ;VzRU@)%jQ&?9k1Jl7?pP&rYMYzKYhw8~5oUiHVpDawCyTBfNKy*0yxXt>&apoP_h7-z8$NVoRSJZzJDs%Ar3TQ^uEq zq8Ej&(%3Wy)4viN^ibuj%#KwPo~f=ocdtu0cW(rgYNN;pSW`(h;uKHl%;+U{e%r+?xJ#x+08gA|Qh-)Zi97?I?}{U)7hVakE6 z+y!oeF+i0>0Fl4CJ_@f%#IxdR-#3oGYs< zCF#nCu`W}qCY<2h1L+F#2i6pSchxbkGAtTptVKg?)I<8V2@F3T!EBpbCA~ohH~PL`elYwXYtI47 zrL0sqhLUwdYz)WKZ|u|ehE_JQC+Uh;$zFxnCU3N9_Sjw+xMq7O-fxW=#Dmt%HYp{2 z5<7_%6MI8`m6IF!Hu=cFkv~sY!RU1LA-YqYP5Zqcx!IW?^Os<8ySYwn^|mMP+q|dC zM?sd!?7EyAcOtxX1P7@3pRXpfQzX}$&M)+iUzHu;2sZf+pQ2&)?P7^Ij#P(WTy3#s zquA5auGg?CD{gfso)B%{FsvDd87{fLnt;D95Saka%G{qyg>$dU5|Un!G$)zO?XPd# zI(=VhV!E(!QN`WLxgRuf5A<31a$Zs#6nNd8HC|L0cC7%f|fg9I70f-(_y%fW41)+Wj5yGgw82%zGwrO7Srb6`}u#=P2!$=?geS~2SsHY6Eo}_#)Kd(#H zM}V?{X}scVE)fdj#p%7{T5jG13kkHxV&R zHl3b~=1uEpN8mm8vJ(-cWR`42Tz!8QI~y}}%8n7%RJ__BlI5`7&($t9-SW%1-Ham7 z`ZGb(23M4k5f>q9tGAWSseAC22jeHzzX?tKI~81hpHjlov@^UuzS4ya1>}Cj07B~A zmll96DM3U;^&Kspp14{~ntL1m`b-8}85%%uH@gx2m+Hk2QwiZwE1JNA@BHXE#zyQv_b*9hLGs8*ba4a}3CGb7K@nCxrxx(qgZZU@( zJqT_v6JQ+=5-9#+0;75HP^dPrX9@Ldk)&+kY+1V=s8s9&U8_j?piO2!KzhBqXh-b* z13!_2px&xG#;MoF#OlLHhutaNt<TBsu8$*rVMRB5|ZxFXMqUwhcyFo%+{MGQgOFq7zE;3m=EmipVzp6ZZhrk z!c(0E&FoJE{&j=}AhC+Y-$at$*9Jtt&WKi^Uj6{`=e>IH%at&jW(*^ErWtaSWx=Pf zZZ|7%ur$6YX_~a-e?>$E7X0`v|Cs~-2SU@a)Z-Sm0spotCrg7@|ieqA~nNjP-Fl25GCJt;)W%8WeCuTmDpRDH21KI zjGg8F%CyEc1%P~oY2`Ru1IsNzdoesN9uzRd4Sj zJRSIH7%8wqWBOF{Q%lQO^VJ9?q{s>50kL<6#J`Bz7Kogy`7M7S*L-tV$mJ8XS#?Yf zg-w{Z+R(`1Bf=38Rbof4K9yPT5DJD5$3mHg%K3Mm$|nG<5ga%@r($q-{WEiK66B=A z2-83VDy}|vqFwzPrP2o+xkVbBO@>KFxm%1q4Y-`3qY!XtGuZ_Z3u}NCqPC@teFt<= ze3Y2$P?yZizeBY2EXHQKtW~h4vMBiK)7k8$nepzF%2E=}h>>;!vV!TKqh3ejV1M&g=^llo5hpi4|EKxcNvvGgdT9Crn4_e0vn*lf&a( z5WTX7H4&ohHEYvbDnX>t0;;t6Y^XKB37Rk$Ct*x4q(p?-LudznV}7dGM6uRv+<{L) z)MS|Ib~SDiqyIW>O=j`F@A6I9zGeT~2JUFAlH^9BM0q0QvMn^s>mE~kBk`3q7}^zG z+j*S33Tv)d_ddLS-pp5h3+MbEAq~V(T^HmqTsah#GH{<=<$LXx(W~uq{q(EpqHoD% zR)K$1JN0i|6oI-@ zCs!XMwSh3Yr^Q4xK65t^s`uH$I__v7=^nNwFN}zVFq;VJX2zu%w)&q#fZ0X{vu2Gs{NxJd^!u@_dC58+#f_xd)fqP!?_?bnf`hT3k2w zEzr0g$|xrA-L1&Yjr6hX?}zEe@L6Ng3k1`VPX-{2;#}+)3MO{}p(e+P2mN(`jqRLkj2VfQ&VS(&!J z8weT(zB!@qK@vKHZSE^Ey6=~r(wEMDO+R?mX@1?U-ILQwK=Aw)*xuEO$;o7YR~eoj z_AS?j9em%P(=^Pt+(UObt+=!kZ;PvX`R36F20gA zZ5{iJ3cG{C^EVmFT=c&F=3s2n*i+JlGt+UWuodTj%x>H@{aB#QFb@o7IHfwTW8s`w zJLP~1<6e841_Bv6=IDvsvAw77vq{2VQfy)N;c`}(6`8rE)6bAcvAnQ4I34TJff9AZ zqDWqf+^+AAM6Ny3G;X=B+SdIZ3llqp?l@}=HdccN zH_Z2@w_%HOlE37E;6^Y~=d&p69;P8xa;BWsyl5KxOfQ67E^G~mPTGYEG4G|YK}f)< zU~Q?8MdyXgV6vhV$S6AEL=cwv zeG6+=2NTT6^P(OXA;?SOZ}L12aI+ZXRbGnAoVF@sjA@2Z!q&W0cQmz+6Yd{k2#L= zBW1D)8^xi2V2d$)U>9ty9|LJCP5s3-r_mVKw5pwKCtQ6#@q;$o{9369uz)#ONyr>yIO4<#V+;7*Rio+b0$M709pV`@PVdIcoU*A84O|*+dMQK zUM&tGzQGdrP{z$gr)jg*^mDSH?0RhRcgjM!bCWBDy~S80!f~t0j2YhRj2WgA_C=oH z`|o4!ZYO(=Q%zd*tq*_6se~L@;;a6bb%rZqt~6^d6n<20H4)XAv_`=KT;k4$@EN|t zpKqhc$}`v@g1-^oO^0C(XL5d^qOD-J*12-8yJ`L%3Z;@aIBrGW+L46>$=2x^I()!U z<;(6%xjkv7uUfEu!pCL^(Lu;?ox-L+Rt|E^Sm>1{a&oGVF^3NPjuc&joVsyLu2`%p zp6#I6h$|14+GeQqaWox?Cu$6(=YW=NmEUQs*f}4;<26>*F~uD@tB1Oh5&O$*>{A^!|SDT~QBQV0Vpj_d5?XkOr>-ztc%s@G+2 zHouRJEoaT&M}WjN$zHWfc<6Kny9BEAM4Go@hk#KgK!A*fNN4VT+AOxZBq>ghtmcml z%D4wBXN{cl7($6W06~K>&&s-gkRWiyrC~G>_ICsI*m<5M!+kpXH6t~ zMHTs}>OXtRaR(?5HE(_pc>%cKfTYzZl_jis79xmCvHbXuwMY{|c6$BZ(D|0HZ-Bj0 zxanP%aY2{IJvJ@jye?pes3@e&8h&yfHvx#PKVZo^GORR%joktJf>Y&Iynbh7A9IqOshUe8kNi(!WphodITzihaA$sboZS%X!r<+y+>nd{z5 zcSl9nv#V&u_Nte@^&ymw{8+o1*-c(J422hNdR}P^3unH<85~sslCg_+Y`oc*={sq! zA6?FrzvbXiY7OZ9eY+>G*-Fw=@lsP(y<0afz8d*9(Dxcw)2`Gwu=cOC!dZ(ezI|RE z_Jz+J1VVp6TMAEJ>EFO#AsNAwQkmMgutaflZOpir>~w$0pyPT;{p*V1qBC8*u8WSU z&%7JTRU5A;1&82BQWsz5QI9lJ-v$vTr}Trg%3|Jnni0zkwO_Y3QlWOe?wsd%ID7-Y z9(T$=#tpZky*- zP!h8VMB>=XeyUBUZ;jOzNKrgkCv&4?m7i1J=P_6eFL& zZ^(fu^!TWuD|>=asqj}Ta>cq;fS>{4vVgzRnP{8y2_*ubekql*@CKndPKka@AZB8T zmJwLuS%{Rw4hK%(oerQNQ_kxGYwW#5LmW=FB)lt%abo73*G;+ym&{!%#GH8v~ z4d_4NUsX+zWYDGhAWb;hE{(kil@&a2XSMZlc;9Q#S>86ooNgz7Z{xnVZ%=eof5~vw zw;g!7nj!Vd2o0%RR}gcf)FQB8XwU|&6gy1u=C+q=)3#l3rpPMO$?TmK1J`0pkps$_-Xm6t9>BzHugx}>^5@Lyrg-o@dH~L zo3#KyC(9Q*Via*Ej#367Q~O!vpDkm2Jtx;1Gs-vqv{Dcz2*X?guZl6aUh}5I!Nc5%isbJ13eSXxdV4kepk!2$KU;n{1(QM&fftWn=O{rVVOfh|Cr^Hyv1s=}25s z;+T2nl2A?4$q!fe*(M_*K)mpP+0|DUhQ%#u4xJ}@KpLyv6L~x^-tbdeQTA>RbWDfAMPvRee1vS zYIvlHzT`GFgAD@wPn{g}kDVE0e$l^v|Fi#m9V2GWlJtxt+;=HfV`qZ@4CT>DfiU!a zo7&cBe?jSXc>XTuSpgi&Z8sy zcq8qS-gx(j9y9;m{-m>~Iuk|Rv*K%YwE^Ku$VXX3pk#hTI?5u7a~#$c_g;4V5`&Yq zoxi-Ri##6^c5Ft*rpN!TVRos6e(u}c>*#MPaH)GE>x(2OqFwV`aMl2tt~Ak_TNf*s z;l}NfcyJ~W;exN+oU=?(Oy@3)!IsyinMEnl@oUEyVTN!-P;dE?}kq)mOZZD?ajXdEgZn@Za58A5aqzTa1;>K;gT25 z;uPx{O>6HwIch%z^LVRMJbojD7c252WU>>LelNzk3n!xOmvt z{8Mkt)N9KdtF97i)(KNAGo(<$+uYAP9`;_dNqAV1ZPQ$!_S;4k4_3jZD)r~rK4%I< zF22`xas)9TJxOXLJ}o2+Z}X(J)W+GSJEVq&gP-FlR2_vG}a%}fiuE{`=T z?IMgG%bjb=?f(4mNBvrbV58yF&#j{GYQzrS6D&l8;){`PHjSUc zd=vIsfAZAK;hw)hn+0C;;l2mVN9ih(^O#s2c&!3*DUx!P^!Lo9%ayafVmyE)2UVKW zIvZ|+Aa4{DotH|i*$ft-MM#e!HAz>4A{-zgQk0A(W>GAk5cq+ZSW6nY|K_X9x@4j6 z*}EvRp?VX0gVWTm5_^6%LUU*SK+&~$J?oe2n64E&hb=AG4<3hKY2Y33{LqWm6xy1% za6c<7-I8;dpP6`fqvY*+p~xrc1ye~mf2T^PH8ocfeWeDy*T05pwXGIA%n|lJmD^j> z?IRkc3~L6y6gKXwNwYOKMECLPuZ<*ptsSRsifj`J43dazE4*2zv22 zr>AH;#RG}5cBRR`N2%8GY5Dyjy@kXf^RWWDit2qt#EHI^I$L9qUBDFcNWAL^6VCaTjN6vg+M^jDisb|;N)V99r#iCskeSAntPaH5M3xh zphGq;28U|yI~$}fY8p3fhMz7OFfG{FoKZpG*W7ciMv(u0x=~dk^6y`9FhJy*z=@WE z@F)|w4)JFGcKWtOK#Q@6@(gpi2|&`ozs{$u`9KVaTulRwf7K;=C(J`IKt{&%mOR;y z^1r*X>%HgQ{ZT3_XPrCZ1>lFN0EPJXb53r#e_`prep+r^vx5vC@^{~I&x3X66#u^T z@BiZ>9*k)I``}-HN8|s^9shG7U2&Ck$^RTJ{y2I2DN+uZ{m+)0!GsBHKcbC3Z(NZ7 zj~kx%s2weJLdlUG>(KvkLGD(o6u_XL>cwFS{_!qx???ATRT)dtFa7iJTPh%LUy}MY z2?oChW%T`@4>P)GPkuID@+f#4T#ax!Ay3Bs^Pyi}TEY}e)1N~mBs}Syc$MV!5FiBq zzAllSj-UbkJ5?pC1pUzW>im*)Og-~04obFD~ThIc5Wo}qGb#eaRGBNNN7 zTwg8gqdz3Y-Td#Di#z~L0|31VT!mrzY`!`4Y>kVr6TA|_TBa1E0U|4>j#mRz| z|9-o_|86PAaXS!B_WV!liwp8LEtGuxtM=gYLmU3Hm5~C-&<8?=Nx|x;$j|ihe{Ws8 zbJ>4Q*bNj^xf@2=&Pc`3z&-C91qz#2EtmagPq%-k&5k3`$p9K~P4?)yIqY(LN@?e| z|LUvU4I$!Sa`wIr3&0YOLxCBpCw1@p{)_7W-Q^Jga?L}OIPt5rJi1;Z(sX)ryW+1M z{3ju39rr(z!ghle9s$5N35v4Q6Z(aDZ3hqjcSk?})@BuzTccq^r065UN%O}TNXXM% z6*q7F=O?sK7NH9q9S~m)vM-HXQ&eRMYvcaGVqWWu%J1X+KmM8&K$gBPWzB=MZ6kMX zytusjy{p{jhdvqq@oWHy0n~&rH-YuaS!?rCpSnN@0s!Zyv27vXmNU-dc*P*6O}ynG zxAV|H2kY)oDG0Q%HMM)4{ReZr36Of3hhlT_=V5z2MP+-NPdFxqe;Z+Vlo$)SAtFK` zRKwivmtY|ZWN~Q2$;UdI{_{=lwsimu{jl{6!33U1sFDM=MoJ3uS+4<_2!X<^Lz++N zHrMr{nT=jc%2__XvyYphXWyEe5Kead0Yr%%zY3FMqSSvb2;xGgPulhF$f!LCP-Fk` zjFF!4%-JXm(Ge=^Oq3VC8Nv7smbuw6K8`AGey#lvGKpLM00~wh zcsdY^M&&jq{F92}Rszqbk-QM5_f?^F$K#L7MrgCK>W41O7`S`CdjW1|5-%qc70UPyTZhyquo_Hv`Q4LJXvDulL4e_5Yzw z^(TZPdEBAY0B(e$pQjS9f%x`+>7&u+J_JZMAB6OwXP8olGP%zGV(TxZI&chT(ve7j zGRe`qU>Zn?W+>^g!z`4Yj1Rp6AW-J_v1ojFA=Ap0Nwn6d)wk{o&O=X;6bOB$rmGQAJ3?dFV5=wgU7P{>o(1T2ubFmFp-2!P2UA;wgRvq0(e~t5al3YAMeFBuv0mR4P;ZR#Gp&Y}~1|CY9K}dHH{i#Mw;Esmy~GCLWFKU}|zUJSv!$QRa~O>Pgf}Gmz5PPdM?&AS#m@ zA1Rr0qpx#`f}Z{Gm?Mt;$;v`Y1<5H=%K_nh>ssr%_I<6rpUvC<`TgMBzqQu2?qS_) z9`>Q z&BHGs9FIK45MII@9V3*xNu@d!^_Z3z59Md!Ma0x(d6TwMeimLtnCwYrJl3bVA$l5G znu@N{JjqlNl`uw_eYDh+t}YMwF72i|wL#>znwtD4ex*KYny=4Rmamtj#Sd06eo6Ay z?W3~|_wGnxgR|CN)ZFwBFV~!Bc9H(%!1Mcd|MA7maodFOB>9&^BrSx+B$?4MI!WHX zZFDw#pO^>oB@f41(D0IPEPA5vfi>UuQq4PVX7jo8{_drkJY-h$wp~CcnbGyGBw2LV z6MehC9}pIkWQsN#JO-9VgQSX|X%x?Dgukt*M29FLwSWGO5CphuTLaF&aF!dTrNipOpb&F>0TBm*oT=J6-l<_ivX5&1XD4yJeqx*O@<`)3RHV z84Vtau$UxMw9()(^wXo$!yXSMIDf-SaxYna%G}&D7><%nl8+7@onHM!j6jSbl8gwH zW|HjrsnO{}PXFGxv`Png#y;-|u!W-WE?)=ORExM;M>8pRr!wmsSQz?2{ z@@S40#P1%xQ}a@E{a1YzJd$L}B1&a+Pvi2PH(g@kflEEb-+7mhYlfJfhS5p3#Rk{3 z5Yi@kgp(vkOif6`ZlsCm@1N`uDv8lawtDT=BgtDdcjVT4ZQ6=)EyLyG#6GHDzyv*F z4VkK^vH!0htJXZXqVl-p%)RpPSgL7>m9v!xT5Sst2x=a4H>oZ=bFY>S;k)<|q1;s} zms4($7@;wWP#Xq>>o0tHsK==%8qE?U;_ycow&cZFgyoDfzc~+&dGz)h!q7=(bdkq< z>)Lyw)2V`oN}~CiRJ1Pj&=^G+t%$=ucg@1Mq^tx@Dn(4Y@WRh(&52VPI<{3@516Xx z`2iD3P)o{ajB4JhwXadyW~jd!t$Ki$%;=&gsyC<~8(rjKeihdb)1%14xX0ye(t^i{y%W<{PE1`tog|a` zFRZOux3cNTtaJ25wZRvx@U(@aS5t(gs&{EB3kQ#RD^E@j`tWO>hsK=tvx{nfed5Ie zKk&Fr5SFUmW$1dBg@ebm&2~!<89K^QNiwVR;@YO2A>R|8xVZMp&tC96EF3%(VX5j} zn##h#W5b;{O-FqCIM2gk>Rm=B*>Ue)TsvY`;Flx^Z#yb|d+*&mLgh6cNoL`YN)eVy zwN(ir)iyu=dG|kGI4ip@S@gM^XUweKG8)|LT_>J5v$pJuL6XzopILjLgHpYD)Ir^! z+HPX@>)F%qRfMLRG9x|Yh-C_&6S?vlK& zB+0lVMrZfl6c8F?w4%Xdj}N@sx80zz*{X>@x<|RIrB&J_nX}^L>=QeMTC4dQon%ig zyd-ZW0%C9rp)p3+y9^Dfn(Mmz4%uj8_U$j8cdwSSN~Km$lE3s$%&z)sXsYpu~v4_E$g{tn@&=kF~Yl^uOVKxmB7!Me;TNiN#r)xH^fjrBZG<~x4vWzPfF zJ?bvduD!}>NTmpiNiszn4XJMI+%5a^cB4EGl|)k+4IYZH7}YlTu4%qt+}NzS(JQ_@ z)S^aXc265ZDn)3F(V@&kIYFwUzLs`h>-s{JNAS2;ysc2rX)B|F__81@1|u1?j{$8o zr26xQuXOJ)K9q+cLjMuUCdts3u$*Fq#Uz=cjRueJ{P589%7=IJdZ7QH717HC&qy*0 zhaQTsRH2s%=+OHRosR%~d7ww8x7!FHj33OptuaHZMsbu8`g5jIbdvno+3C*%LYbRX zns4Yi&D#}WQmO3954Jkr+39>|FO<1iS2Zu%N)cwoxYR5s@c7d4Y2W;oUs;W+cd2I> zjq>=SAS@=Xsg@IXy!Zag-CKNfZ1#;~X5FK`8a#ue0Z0|bLEtg&%t6`Vw}#OYYAr`g zM%TMgHx1!>&OY><&mS>!5x6s7?Ze~#gEJyncT)e_+U7mKs@>l)v;lg4|8b5^JiTVd zwLTo_!rX(1f4{g@j%Yl0WlBV6|B{i&$>y{~+;->i=7l4#aXrA=+Wvbrd53j7 zUPk=pdtrSKsiuwq?N(SE9m2zyI+jXPJ$}dO4iR*kIANAYLzWn!vMXZzaUD*N#OYB) z%s+KTuE*s|Z(2glO^+ht+;?|(se&XQcznr7>Opjw$N0DI7(rV-Ir)(#E`_&SdT@IIyj52DM6U0;~%$~GXDedJkZ-C(QtPMze#%ZP)fEE-AcHeP?E9 zL3x2tsqR~ML4_X5W7+EYxgOQFditJCa!*|Tz?HdFLkD%X+TCJ%MedMFdC(%|GS{|U z+m&h_DwSHd+E&^!uvLSEtX*Zl+UE-20(f82p5eKhu0_k>TG3(&G6wl7V(+c*Zy}1d zI&_}T%lAu)An&rR6mi~@!+gHr$9AlYU`vKnY;i!dB`PDdUqoRVbCaYND{?HCZ(JoDQo&okJLYm!+wc<6ZsOV!iRR2B|fJ#l)caP3=mvaN6xBu?umuJ7-L~m(4ic-P8;a7L_8rwy( z722N0@gIMy_J<>}PZdT0?Gc)4QvW+{z4W#J1)-^a_UE;=nLi4nGv<%KbzkpT=2|vb z5oUOduys+C+Es3(_pX=Z(Z5|^yYb4ldenbf+pw$YQ@yjUw>JAw87BrvzcycgwI{eWqntwSU&@sZ+^m`gx-E>ZK;m6+g z8TJqE#l7rDWBWIhnEpdr5;|5P=8fF)>;tr&|b#&3fI@;VDF#x!_EQpG~)eFVejB404Yr8AVqkbiC8sZ{wZzV;|V6{W*Z1hPJ4tIS>N}?9t@tRa4nH zhoZM@du}Hcs(7&fU_Sx$BZESpaqGT!t*8HBDj?YVR3o%!(Nu~y&4X0WG$&5TeA})% zVI;#gKSoSV-pVBbO$n8E8Nu@~no9YV5ti>;xqV!3m($eIl>I&Q;Qn{9Bcxy@xULbCJCZv2>! zgJMK)zk119=P_X&A3V5XQA8Mbq+;Kv(#8mkGB#Q;IPW=iZHc{TjL`XqYqj$^NA(y^ z>v`N~=$Od3)ZL1}ltmorYmbB0)xs5lo{TY|IX9?AK=b`?80S{7!+d}UgjbEPY?PtB zjP{}?6TAenum03?tv!e-i#X@lVuN){EZi{l=wz18`@xTXZ0Wrxx;EpTv5DtVoE5QT zV?4*co+t&~9UlgC zwI5j)`*WQ;Y2R)z}q>hiFxQ~Nk>TS$5+rpHHY0y+bS>7*|uv@ z^BK1Ci=~3REJ1A~1M`1di zixIkVqv+~UpPt!N3AH}PJaiPMBfx4zyaHu?d)zzUnttn!zzKb=juUhoRJ{gZYsk9h zP~AuAI-I7e9s%e$LD4bd)Vr>~J=epv)Ej*9eBUN&-JY5})#U6owyx%@;{??yM(A1? zMgVLXKy&;MBXl$Ynk$8Ic^Jl(hx9l=aLfRj{F3Ao^*igc&&UD_^VQ*qu zvc{b+3FnY?j%zd!mmScVuKH#`Xq#wrTz5h!ze@_KdN(~b-D%ZkUXL-4UDo`^9{+H2 zgL;?UKB{*yJltKH<`5rjAuJW{+7WGcmjdDk4zXhaffyn{TTVEOqdeNBQnV`or}@)g z7?WQ8)OP6^YZ?dWT91unu;*nv8%Z+o*fHtX@7vC6ZmFvFKfr^CFRg0aZHOdOe#S$k zaxx~CN+pTWNQYlNd;d$HT-dqs2V3hUNkh*O;RI9|$$$rbXxH&q`|xm5d&c;K>RJw9wwDQ=ZYx=@vk8#~6X{c11lV{=nt9w0+d*b^m zYHQ)}-S7(tcg8E19e2s95q>mw_SDa+R4Th#I_5F&@|5{D#+-cO67ohA!3=t~(bsNnk+SM-Gvxuc?p9=M{3ZbotSK$$%|uEp6R?gn6Hy ztSe~#&Sp76Q{DRDV1~DR7iGTje;;zE#&jHi;z(LoJto$?eJaRKZ>N_)kDr`(2t1J% zZCEO2D=E#Ghj|8eVr>7^!f7k|uBGy2kZWtcf#Yf9^!!h_%f|?QVaT z3;cNIs#+>rfdUVX!eD8x6sc6M+|!PsA8Vr9$u=(n&!Cizx47JOOPcS z@u+Aylk8WYp4;U8p9qmoa&|I3c&p7@H`ggf*d7#mHA8>vsW)mHK256|CU_z}u2|;n zy+rf6{N}f64;~TnRo=#-SRUFVYabjVoQK!w;h5Lb^KHMrP##WO-d(7wwFN}_>WAK` zee*-F`x1=#@$5$tp?+m2D*`|InCSs;xAA9^IUmbQ(^f2sJbp27dUN#Of<9`70k2<{0c*@^GX zt5GMlp`{0p_G9XpUyOiMJbwlBfW02^rHOE!#WDnJWowp6w#&{d-HD`ky&krHskVYt zw*OJIF84x;2<4&q+8K=^!d8T?~IsKK-laDtr}mvL7CgDIx#~1r~Q52qExm=@7hFp znLyjeh)34`tQy|PxunV4r-pF0^7o*4LqW$P?MDEx?h7jx=iX;CdOh7%)I)2nt=uG; zmZnJs>wa|Sm3gVz%4Tfo7=a#uBR$*AfaaWS7o|cBPrmCY1K~VpYdoO2=2b*6CqqCF z9q+uc%{xuD+P(c;H7ZMGF*-uhoFX0=pXXykyJO<+G0FYN7@;Fp(Dj$YdO*9;E0Cxk z_G*B()V9J_)`R+1w_5*WEsA^1gF<{t<9>SAxb} zM}4+ZRFn!WGEd`b|DpX#B$e-R*oFh~=VNcm+jgE$HU#_%BTE}W+6w~ls~Cotl1>`AxiepQ3 zPVyAXL-W!xoQ~~c9(vnv5A?PkQIKHJ$SN~LYBN~ohj;{jV~PiChReC=wTZ7A~4 z*|yHG5l;PL9*AK-(c8Z|pBo^&y>&H}w&&W;mw7NRO{LPti29IuHG14fcb-$*b$Y1t zTK^4!@=#y0Jug4A$Y)E2aMsOq6>zW77d81(h`Xtq_i%;?Ja|F`H(CGP{M<=FDmTY% za*k^VlpsbjoPS^>)8D_+w_OP5j5S8UCY&j%rL~-j2wS<)k&}+jVuX(RbWE*d?HJ)^ z5H;2uj1x&~PBB8)5Vcj))-Fb9KcOoH&eAnnI!0*krM+5pAEn&24UXUBFfN#L&aRDJ z9LiZeQQKonk22R6NHkx48zM$%i>xiRzF8F`)S^26RGo?lo>s+NZDwAhRKFOZdZ_I2 zOCg4-XAdpi*Ucuv4IaHg9^dX4M5=iy4@ec>XDPODT7qg_mDV_*oJro!DD)MVYJ{%V;2jpugmpc@WOCL`^`$~>+jU+NA)?o}?8S@q&5C~e*NgL+&<#Up zb70w)gD%1koM*UG zb72gxykn_!M_N~h+ph?tz0y-Jigv5&`F=vgBi;QYbDEs@PaZNaUxC`-j2||+1{foB z4L}i^lOZ4#%9ecw%7A@edp%Hs>Z4kk)#ai2YMEE_usbGL@!}2Os@)|gRjMlmx^JNC zGp0H8fRC~jh40eCtIJ$HL($a;%~$JVH6k7lT3-SLJwMh%dmQa$su5a(T58n@Eob;H z>xXOS^Li6WmFmeFtjX}43~0Vn6(L+XTYc90SZynn3U!dBsS?IK6rrt}qN@?Atx6jq zqFqZ}<c34aq{Ttb6@qogf-If+q~}SkM8_0H)m)w zn~27VdWJzyL&Q>Pj3V?jM2yhW0($mCPk59O98KyNSJ4q7Qw?#Ztbf_+R*hQSevadI zW$a9mmY|kw#3R+!WnFdFb89g|Q|X$u?s>)t-JR66Y4rs|pnfPq_c^iR-F8Nw>bW=F ziPdv!#)CDAt*E9t{0r~Zw!Jy*6-NlSYOp4MlX%jy_i8JjT=l>NC%50?m_$2xl7;N#$Dx>H(=gi~SMZ;26FpVdoreLqI%O1!S(=<1Fk zP=eaZXzQb`wjod+Dv9puYR^!O&{W#;#fVwQ&a8#64<%WB$gI4*0^*i8+O=0IiSB@_ z6^sWo*OQByO83G^WW@W~_7aAA33%u}pq84}72^TK__Y`1ryfu?JPm;!wR{2s zQl0j*Ik~O0b=FqfcmSb1G~f00&em_MXJ}3_50zb0eRRRa?fVbsS8*TC{uSY?(oSE4 zV;-n$tWnw;Xzelttc$!jDgv5gDui>aRYoud2#!U7V2fdhP(Rkx*k%C1Gd@7@>`xiN z7$Eet5Kn@Z5sU$XRsfIoZAnoQm5{An%mWCukB&0g@|6)BK|+#MKlvYDYRHLWq8On* ziu%F2tv+ptP=YpM(bhm)2}6YPFodSkF;R?ANpysyBc~XlF)F*}6eCo2tt;B{8G^p6 z<4+xzawKaAXs&fl^HqK^0;Q%s8Os^rEdMfs+)>W#`!pv*P!HB;Ahe!SKSQua={TCc zq&{W{^3ZvW&UZLZFa&w1zp5WIrx-EpV{@B4*QIMcZcNm=?Wny0coff+@T`fhio`rz zzm%VJ(bIpfpD%p}3&{9fpQh3?elZU{HTA-ZnfY3l8x2&fU#V1juSC!C#ym7%UH!uR zoTnCIgq{V~GgrzxMyMXjLwOqlUZU%5T7p`#F+y!}|6ec8%l}uOo>5We%0snP35`ea z5<6L=C#&=%PmECRD%E|%-0pj6UkyA|54De?jR$O{JT%|l{)?LJ&spksCXp{kGVym5 zjY~b%^GV-+>WwDv`RG|AOAkHttAYB}KUkf8NsLgtyV+KL=1SLLV?_He7A6_IggV6t ztQqGV`)ctiJw3e_Be34e^H;i(s&{CL2)os$=LPi~VT{mwH_AhAlEetLsJ*cj z>l7p4OB~m#$5i{3)=q6}yB~3|#`MXG}3?;}l0F)YM^t*rLUv7Q3-CBm8 zr&L?%N%@FJYHvDW#oSK$Yn_h~SgoD$)-`_Ch!t9nMG=0|!E=3h88PAG6LSRnbG_#m zBh-p&-QNDW74=-}f!(%G+#V9YQd;Z>H5K@={Hu9r>!Y5b9uw=KEsmyAe~S_Azx$eG znp2EWJ=6-SQ;bjz)w-Hfj6k2kl{@{)ZRktd&4lM~IH$?)kM&NT)#R7OdMD4$>6nK* zjo0MY%f52%%$z=J?aVx<7@^#?-BoF0gvzd#M(P;XGQ>SPFxGN_??iS}KFfK=!I{=FtwhoCKIcX~26M(AxI zMYvgUv;7*Gwq$5=xLS*xxQZL|K)#%rsw4<+znh`lRVvl5%!9MhA`i}^%Lv945tKGY zXe(Mokarou_7!??R*an3y2c2k;tT>jcw-&mW#2`aBd=G#H!Jsc&hCJq=f^y>)@n`F z`X3|U;k*;4r!lq8#|X_!d1$*5Bea##RN&qIq^xpRomASGhwjm9Dn&<#OuZfB=>1Oo zhqttPevHsmipCiA@h^n+{TQLQS9ScL=oq0R3(dD0q5CS>5uhHr0}=Cp_wlBudSW#X zjnUQswfUpBgcBDr59N;i2--*YLSlsS(0tJ{aJ9Y~p{Z2A7=hIR);rL&f=U}BoFp}B zh@A$ugVhM#T~Kr-BD%?{J4(7w6-niGU+u=S-eb`F4G|*KGEhm>V`7A+!oC{EzIvYm zhKKi8KhGJ>y|BNcD+fPUV;haSd>F%P>@W%(K+yu;ECp_1Tqib-3R zD$Jt-f^#aBu#8aIRVU_Dg$O*TEqGABGJ-Lxtw{yV-OFDo-wLxA4|*E5Cezcfa9H=N zKRPnKb)WFMn5Bx}OR#Y8*z(I`(z};#mmYTQ#rxaMIpv}Ewt;wd%$RhILqH?V%T|YC zbdtPw)R^@42e(UiKljSJY~3eLrD#JOa_Q{5EJoh|=xJC^(40~|w#kD1O{y57IVH(z zr^oXxJxp`eHby&JopDt9x8t|=nj500VYdu>8Ws+zmY#A{y2&X4VW~7<%nj|!1+Xc8 z;hT3huhlPj;H&I>(IM0iL)gAE-XpU;XrmQz?a?por}rQ=UtLG>BKa>6-PrrmotjF~ zhEN{BYK$U`CIaJW$74U!LvNYlB_+E(6r_T!W*q&}oi4BZO?yQff;?zli-~zyPAI|f z3*}+^jy(-cWpt5;X&#rF*|5k%W3>E@E+VXKJ&(3li1l#tHu=>B^Sw4eK?+IJk6m6xFsDws?$L~&>m~Q>h$UgAUxxTI%Sa`>X7-6Y8MrbMv z2agATwRt*X<}tqXbq?5mZU`QVuv8r*4Baup!og$pOGl@t-LQq{p)n?vu1tW3A}p2W zt2%+l=dV7bZ~8$KJrA8PV@%CcK^6`kim+52BQ%wTgGW03=xoaaxAZ)0U8G}#(Z!Y2 zY}(P;lP!eKmyK35q6amaV#JE7j0S5Tgv* zt!1kmVW~PsXetW_kAX8MW`FAL_P%Q?MI9rIPBOJyl013w#O(VA2ZY8Lt!PNK;lW2| z>wdCjnz$dd4LevtbM9sVZ7ZqM9OA$hLSu|pG+IwgOHejDrJ0V~h@MNw5_(XaD5nb$!$t_f5vrob339wA&d^{&;sz=WiPp z5jzdOr0G)i0m1!j&qn>`>uHg=1Q~;T6``j^VgzG~2%bAB zmmp(`2%d&1>%o{Jf}WowLwY~dx92x|ea+SL#zxn>+zzJm-POJ&N|I9@;u9?dV^E(- zqy12HlKf-Rhx&%J;h_l9P6yYvPnqyk-xXJUyC7A)OV7X>jasV+i_te4>s=-lch3S)+a(bu42&AFzBRB8pT6qb3Y z>`bNTc;(LC_3dfcD{0|XXrg0;rNZg)cCP~4+&M^Rrwuzs*xCG!5q2iIV+2x$ANoUk zv!-K&-35rzOpm|y2w~?QJ4VNU0kxH# z*)AfGFA<6kJi_cHmP#eDQ`a3M>@;?fhvusY%_-(#V`?3%hcyRE8zVF?mBh}A7Nt^2 zR6@0g@qk|`LQ{bUXGn&CmncG0K|=aMj0l?JY(%_LptJiJfq6#QqmL1qQ;fiTDV$r0 zXAqiGH9}L_>Q2$R%A@`4u86RikgoOUEKCt751scFXL{5x)L6YF*8#nuqFSYXE91c%tT|scbcY2xnQ}BjVOC_tG{QaF*X!3%x$W z(VvH3y@0594IFS*>lB*N0SyFs*nVBTi#(jPIc*3R>Un6swo~1X2N9aD=~Ts|uetvL zddJxiiTnCk(5WodfKSO}hT~^ZrE9u;cRWCX6u+QG5FOcXR z#rBBb{b=Kbx?9|ypYoV=<;MHR`Py50?Rh{CEe}_M`Fmu}3axUYb?H~mR(T0(D%B4; z<5#R7Nv1sLq2)5y^3YP#(hMBPBR=ejVaUfZljXexe@YyF6G-`#x)rCPdT-zKHHeCbUtJZVYw!NVW?HRMBH zSDtoNuDK#sPMFbjNgRUhArZITIXp+G=GC^+RFnj~s}ZUPzY_)ozaIu!;_~2^zL1Ld z!9nx;VP(XMn=jAHW8m;@934vVD_0NmbQy8bltm-yCG-1_YlDETx?j6(7_i{JdC#wM z9*TJH)Je`uSRP1?9}sJ1T$>{(3Brj+x>$4dtMSKmjHDjRR?qiz8L_(Ak?Wy6XoWID z>qlII!NHB7vua9pSkHMn7O4Uq+AFq7<(7dlKm;0V07NHYEu%FW3*JudNTpI)n^=^JXHtRSIaK3^ z`FRnclBk44mk~UzihPwH(Pir%I`3LvpMl`*0np5;%wyLV=H>{VdsTFq2ipv!%I_OE zn&>h@+biB5RCF1kErXtvCAy5z`oZ(G;6Zd5q4k4jc7Y(ej8Jdq>0cm-E+f?2d8-i! zqRR;FGZazXM}-r=p-%AhFL*Gg_4CmFia8~TeltwpZ$`f4Vd3z0MOdnyhNiM`)WNf# z*t+iBKFi2rIK7~^`OD3zk!TDU`uZNr0TU+1fvTZT?l6<|(asrRv-Z45G(AklqWc20>X-K6v_?%!4@qq#Ysfd6T z3<0U`yM1()?j3k&jM4a>SGd&}tUL9UU9<1J6xOLt56w5p3=JN-PHi#zPJg}2a)Rcs ze)Fj8?32QII6XULbWg+HXMkTRLSu~96G`9^eqoK*?!KXZa22?`A`DDG=p9~rxxS}i zbdiU~;QS`nsf{ioj0et<*(?7>L-X*XRF<#uGrEY-7|R!X^t>flL})5S8=|O(TGY_7 zPHu3y!I!gNKees*c0I*rbWg)>96~BZXpGS)L7ssuN`&qjQQ;;ioqLa5t zZzqjZSZ82(tcS&*7FjAqS0gkPX_wlSpc?_C{LZMp7TNw4Nng)T8Ita_RsJp0=Jq$g z;O6=`L(jJ+{Z~p`;ox!0=EJhjuIut`3EEfgpBdfLuzR=QvH!ZR%%MC&V~j?dXgRqt z!@FJC;;jQhzsO~D7;AAZ;!aZy9F`rrVL)Jv!x*EJ%+S!|mnRL$uG=PPuHT$%|1CP` zvF5lT*^hP%2up=s#QSR>e;~OJcC&Es&{kA$jT;>-9c)-^ckTYUrozef>)oyRH!x># zJamqOn>skjftpGh^&B)#bWln(y{>KmbsuPhZ>hUHDRJ{As7M4QW&T3;%3?QZQ4 zN22{e+r=*hgU8B#*RF4tx4X5OKML>X;SC|SsP?)b5O029Sc-qnXi58g<1fJZmO5oi z-9F#&T4>-0J$~^1m~_rFVf1Qmo$KwIo`!`N5qdX6Q&~9l82Cn4`s@xt4|_AQr(xRq z{-b3PAokpGSbEPtLqBK;)zFL$4M z;n)?soDUUPgn*O@go({{^rdZ8?F*CmvfkeCFFf@I35W*iLhqK^P4l zim;d@Q?$|GamYDyy5D)>Y|q1_s&{EBqru}~L0C-UZ?y)^V=K5@hVKqahj)+iJj{yq zF4fj(@KA)s=r zJ{u4kV|2aC(BSdO&e7S_WgQNiBr1u~NfzI{-@_r!Zy_|sXkS-anuEvQ>8pMJ^|7&@ zhbzJSM!)m1{C>WL1F?@FES2|?7TWt&j&?2FEsc9!E9ZIWU47f5O)?7yk1q(qQq{XO zm4(B)drX_vclloDcpfH|=IcGbB^7ww;Sf_=2+h~f$`3rI4jPob@Y*QP!z>!U4agS+ zEF3%(p;pvX77iW{-giv;$Wfc8D06!uwBBWJf7ZJg9>$3chgjA^SgIsrDwk2l@PJt0 z5L;A!Eo?PlDoSOOK-n;SFeX#vuwR@9?NdwUh`pxr6*q$ z5So{!VtCNwR}L|~fIy5Pj9<{ht-YpPd(~9>ny%5l1Rdgx)@rTA_!Zh*C(8+XxO3s% zJQr@S>DIf9w(raaJ>0qQZk`LbRDQkPjUU44ab4H&=fb;rF5F_`d@UzP_4NOJvU~L- z;as@s5ldwVXs!s0Niszn4IZwZ4`)3`uf`KQ=zaL|mxY6etLHhwQiYx~phFKwv~RDv z*796S_(J~gCL*+XATwC@K~_Jgj^$lu{`Jx-hV zIl#m9gE_)dh4EE*#UqRv`MQTIK{tB+m20ihUSXuiS4ZsLL>MQAaPYtg`4=sOr3&Lj zrm}F@YQeB`(r1UyVt<~?uKZNmB>Ax*ES1Wx{J`TSH?o}M+IQ$-)=e_Cn}tJy@;#|J@8=snr;X$hA?W5D~*1np?7{d4k z&EH=BvGm1z5A)X5RQe*N(I~;S&wecZa0_8ENv3F{p@+8Z?oLeW)k}XrCVv5wtF=K7 zZQB*0sYnMs+?}7iZMPVE`w{tCPLRs&jCONp)bxm@G6XbtyRY4HODLCJ(ME&Emk;jD z-aB!4HfW=34xF02>2{}I^J~bu8lij8uGSvVnf>jX!?W*fIs0z?GK{8Dw7Va5*qH1O zf7mX)^1MCnj`yIha%;Ws`n3#O7sdSiwl9pye*aW>BSjHLV-4BR&J!JCkwd&Ze~$w+ zFH4nVhKA-xIjP=oQlZu=!f34TlLruAJ9lFC-G{}V0B-!of zM`z7};fA!v7_I1z5lQmcZ%1~oIccJ|uHJmGJ0G~4!*4Pd5f+oUuZP%u6QiMryLq4T z=DpoSNiw^Ml4KSR9*VG3@vR*T2ammex6iXN*R9;0J4IcU)fN36= znw22*P=v+A7PXwf!`%%}c{kkdE2(|pyK`G#9|8|Ws1-Gpg@Z>r{pgf$SwKU+{)<%M z?m;M<5Dp%SuvGD#C<_OVrDG25{{KD`-sA`xhBrQ8X}%(9;ozYNOVu$#Q&~88?6u_< z>9$9N_cAm_b+T~qP=uulUcwt377iW{{BrYbW$TR;)H~V=rAa+396S_ZsY1P@MJ*gW z+*>ah-+Iv&$LLVcSvIb1cZi=q8eWG%-DZr@xb4Ok4pNO@b#!*!_2C^BO=Wawuc)7Y zhb807EvBKSD&F(bS5;I)E5Xp((bDldES4(ND5kP-*vh>WlJcbx^*HnPP*-?|CPrAQ z(3UWjg@eaWdM9T0e~oWh@LQJkuHx4n?Q5Hg&@b;9t!Rt@ems6`_U8ew`1M^)WptA1 z_xxO$j~kmcHwp-iF}mJmXz)1y;oY+He|Tt`xSxGNu->I#7Bm`&F%RvQ_09_ji%I0` zh4n7W3AXz4hOhMPFg~pB8lv8%-_$c2JQQIuNv3F{!Q;w#hh}>|w42vMwKZBL1mZhC zJT$xV;eb$ijjneY8d81f__TX|Z|Dbcizr0PB~|^}riFutA}m$COH)}mcsOrQ>Fvth5N=kKe<>C`T#ZUuqb!xm zuKd8md3#E4H|whANv2ltH7aF|vKX8&Gx`kzA3D)1t-ac<9^nC=?)XJJ@?8!rwHYt=p^~~IUh*R-1_U@x~hlK z_)^#0{7iC^tZSZs?^h;G_K4t1L0+R>naiu`?&Zh_hWEX($;9k}E2i8F|DlGOO5a9K zl9!X``v9*DluAjrGlC&B^U~Z96J^d+*)6RA_5iQbvbfjo$!Hk_p?5%BFP& z1X^Uq7#(^X=9DBip0P`|(eFN=J$&whgH#gbf&PQOkR;#T?S{UklO|*zx^?X5R9@9q z^>gErKTaLK<+)|+YB{T=U7c8Ya(d8*Un|t8&_+=wt5NW)IV(<12k#URT5~j&g`*CB z{JrkJuKx3~_iFX11WgiGgQGIPWnFjoJ{wKUZaDe8$amZ80jXLe8LtOM6QTVGBMWOw zI3|KriqH{~(TZ+qo_(UFxt6ohNoKsk|2 z?$Mf~_0eh_0Dt%Jt9Sw=WH;&uVtddbzT6&6Jp-P2ICA%cV7*Cs5a~2hWi3-y=(g`=DV->j__?Q z!bw|tm&)_tnIiBY4IX-?h^WBhM~^mKY~V5W3Vi+2tw1{-q?sCj@l`83xAgMLzeQ)5 zcn%~+*vSFiWe+<7Jm(N2z@73!b6PG1GZv}mI1<9 zPSrfr3MyfY;Aw7Z&QsS2KXv6FYh|e*$=EAq`WTcTt)LRdJpTH`X720Kc$+`?F6q0z zw^{9{;~8!kU(a(5)iaW`TEv}wn81GePCVEF`{?!rOg3#d}#qAWV;C;`+{NW z=`IWA=iHY)c33VU!rSw3R&1ITrz~RI#XAjU>lW(-y#(Q;O(tkQ^uo<+N8Lp$PFY0y zp{1ME7C+2z!{FroltpYU8;?A+Vm6IRs(V=9{ueP{cd_0-iYjvfq0ItUxqq zdSwaW?joADq+L!;%gHctdAN~9UN&yTA+8 zKq^XT=}{h+)V65S8!+CXmz=VA%jOlY7kQw}x8C5+kpt?ldRxWqp!Gh+0|=BY>!8*N zgtxCBk&nK#xNpc`D9MyXT&po2MLiNhxLT0w6eHk2lnP!#Nwk%S5uvYrCHE3aq85n} zF?XpGQZ_L{d0;J+Jk(=igtk{&9$}t9X$@0P2M*XWU-8mZinjEyE^NYD3tO?KBAj%L zX!TLvR%oL*;?Nc~MyOP%chp=prsK`I_7~G+6oBT2jQe+gw7h($Kn~J zVZtbbc_CkVWvc{REruc>3GEI9Y5U@FQFDyjSc^1YwVQE3tyOtd4_j?3@=%+!O3=%$ zI>kIx5A?6}7=)89BNzh&{R#-uWrSK+OOter&^JZZ+uga0+UUQnb{^DDb4ms4vOKhG zVje1QYsBHl>IkRj#|Y$09y(7@elY^vX%j^woH@k^aA*C1RIC#SCmkbNW$R@J5Asvq zF#>tf?%=_)K{)9cfxIXcc+ft|yNsZ`KoG4t#R%ozDvu`08R4vtF#>r}5AYyAgp-aD z$cr)HK{Uci#|Y#_n}7#XDeoB3lGm39cu;eMla3L{iyDFl`6=%hfqd!Vg;9TAgJT4^ z)8mjY(Fi9UBfx{c)EZOgI@}KgNH~cbyE=qZI`Isj8=r&1U$$O;iN4+yrfVDOSCkNHcYS;xq}DG zKzUnw@}M`kR7=)QUii?j6f=S zHRe%@CLJS?7t0yi{%3kvURH~c6M4rxRQA^F&U*&JX^|L#yv!S-Ixo^1Ca$&6gAyVq z(v}|91$WvVQfX`Z$zxlhr`x;ffhO8R4%G@JbW$tO?fbu{p`j49%K(GuD zPCDiR?#v6?k{`lJ#|X^}JXjtGCtXG`2KiDF%_&A`Do8~MRoWPVyyzv6ilv5d(lG*g zQ7Z7DRLVO>ATN3ec+g)FPC7;)FG>X-K#)okvJ;#Tv9Pq$lJQtrr?C8*NIJP@OjP;-QnE+f>#S?_=#T}G(C5)B0D7*Vt+ z^=Nras|7bq*so=oBVU$*=49#ViPR9bQZ(rp(cQs$TNpJ@ShszRQUp@5KB|YB9`GwIDdnNQU zpjnGRv!=xeXP>-Po4?^D?u(wmk9ji`mW~mwrsn>mZyDHa`65DNT)UgkAT_6$$7w&C zQ+w__+$)M}l-9HuapBXmYq#xEMrckkLcKw~ME%VWC=b;`HRsz2v8{>-d&{8SWw-m+ zXDdy0-A2HhxEMkzwA9wW4DKf2)JynR%= z#R%oDJd}5gP#&t2@{1AiiKb}gm3w83n6zwejbn9P>vQLKYr72%WB3>W**VH^yNflR z#=c?b+}bPttb4J{6``rX+fGLtm;9tt6CQ__LH7#V_k)^B_0Y9U;{e}PsZPwKxU*U31*6E#Nl(41m~=BuexzZju0s;%Z^ z2-pfS;m%n6!iGv~2DVyWAP^iW=29`l8%3Q0IxB)PG@xm!KB$43tWxwb`U9dOJ>Ov8LiI z4r{O>pt*X9N~P!+0c}~I6#>o5H40Mc2^!}yO}03=NkD&#dFVMJ)f_Dm&wIrP)gz$8 zeG_fF3{kX|+DFlbfUWe*ot}Zy@{bW}MYWaMFh;0f!DDD2MaKxYCR5`|s!F0c83LMX zNol_7p@x9wT7r813g=GRmq#&Q>QU|ODzDZewL&bFdV_k2)|(iir&!&HBOfz5-uW7k zAz&+=jV?cBZj&=49c9Ld-G6*>p2}GvpUa-L_M&`TY6w_&-f=UV949J5bBYm~mm*ZB zYJ{fZw+EaYg%PrjsqK}MdY8TXQAD`0T65;B|M25oER{niCX6BJuyPP zT5Y9hLlnJT^Hm8AQItw$pYinUT&Eb}Pu;i1S9;D8<r94#C%TbkldDdOaHGLy`*j~J zDMjddX@Q5d87jN#6eEgo_a(idQbus4 z2t1fC==No8wL_u_jWUwqe|-bMrA#bz7BxfkDa zgr-uo(cs};WAEc@>?T#Hsq(6PzANiq4s?hY?fv#vOsuWt1Rnb?9+YixR(Orwq^ft> z$RfP{%`s|>&{T>x8a&*00Wy9`Ku0o0hgZz`g@G7hBNj~+UKqC#8QyQ7)_srvB7%;6 zj1FJ(2%}m^^+Q3}I8jrDm(p#VICAaTS?$5u-SA11D!kcF*_B_C%y5XW-5(GdW3-}C zKOWd?bhdU`M+zQh-MBo|Zb@>#L%iQYXpGT{29FK?d`$M{r^3tJwg!-7r1ADAXUO4w z(LX(NOt#<97=aifT-|i+$L}5Da|Hxqh;RnUB?O%$1K#M$ z{^Y*2vC)^#KCrj{9ryjH*W5hHe>bDauad-HYHI8-EZf^6;nCI^Is~Dq6m2wkZ1vi%+0d2YE0v}pUOAyuv4jD!PYa=`6m2wk9RA$n-G_Fb zo;oWgy02!tWJzW;d{+?`lVplE8a$qL@6bRmVTN;3OGTJdXX zr@D89-*xW@12L)p=9;}Dd_ygKm(KasVA81PZ@6}3mHP(IRew2Wk$!zhzuIHp<^kdf zmwI#e<(>ona?XL8O3{XZ9zWf$Gn;n$@a(!>rY!o)DK|PjaJxI!h;!_fvjDMTR3aSz~i&7P5htM*N3d_i(5EDK#%6o&g{?QhG+lXclm*8QL_)~`J&pF zXTR;F1>&qdI@2NN4$sa#ecQz!-FanR9`4QMmUVj?_jLV3^w@D&XZqe5!?TkYpL6%L z@~FTC=eMNXrCG` zVtBTH$IJtjyGr%Zo&Qz)-F3Jd4qN?ebZ7R|@x!y8p88$8f8<|H#^~G8E~nN!5s0IY z@67&s*zoM|^KM+M_EEcO$tKA`yLDzs@9=Ej7aw1QQ3k)^r96~(l1y|jTCa64T00pN zd$}6lPF$W_p|}5~rj^I2VNMTc-Am4$bhpMRLSNHPl5MZ(%vNk2c$g$fW;DJAX41NH zKe#hp?rPLehCOqjmVuT{Z~x8ti`_{wY~Rjw=68o@Cw*h~A}uLJAO7g=V%;b9>`Zq( zXL$Da@qb*TF^X7z%8jnfL%nhDI60|SIH|7wKi3}UWY4|NX`cI)`dgBG*xBlBXRG~= zAAX>EwU(x~814oB;hpKrV~1y-zV=c#$}kOM&F_}-n7v15_90h4E}ON@V)Y-juD(^C zB#S=Onf`m`@a)#tURk6*sygA+0=*|m?mVC~eZYCit}A-)R&5obb<>SxoK%N7slGMv z`UA9;QQy_p75>Mcdp)WCe)Hn#XZxB1&)~NK@l{Oyj(d!F^?S2&1ZCHpkQ#S=!VlkY z<2Tkp^SkPx`So<9tVT4RyE47MBWVCJ{3cTG5zM!Qi1aw|h)K2kJBE5m9>3%CT9O2M>Ij@1LMvL-LlMhX&-b#gUk@T633-5~ zgk?SMTX%t%3aKawXy#Ow>b+AZIeCLcmwn_}w<3&|+F5t+L7pxnHt+dWt~q%C!JIrI zDi6j$lKK6|IdtOQ(|39(j|j2r3v<0xMLn1kc<|@ErLm?tZu~ES@LzP~91xy3;o}p1 zI8)I34q$=_~M9*9{!#lH#XQ*loR;mip<@T+?Z z&->$?R=-{E&HD@|)e@6R%L5+AdJcN&iXLBsL9>+rkGL&S1X}|j z*h<7-4TU!l0h&BO8-kVvw<3Z(5MC|Sm=C|c1nm_h>0CO&`BvvVZA*2XB_0rDU!N=Wdl(!8r6XJ_N)$c9E zG4_T~d#O-Q^rSUtzNLnifvp6!4pJ$?txe}=wn4jF!^&tt{L4=q0?x2V6T7sq?t=023QXv)hAZ^{R-laLkQXz)iK~oQ)DPc7Nl5p=I zbX(1{)oa&|oc*s$UEAK%cxPR2jWoiyKYbS;9`W#qp2HT72&0K9i%3@^w%B0Z66TAW zdZZEFo(E!>@4x{+Xp&w(V%?7CxHhV%0R(9vR)6X_*AIrV4|S<$$^%jy#m%EzLV1PJC-ma4OViEC@a_>;X1q>Xq)dMM%pk1uhx2;WFvTbnpxR_=*m zyZ~575<8t(-6ecK4D@pA9-lwcUO<=YAEJ2W_RL z2AVmQrD6;uS-K*WAn1rkq=zCX6%Z^N@Zc|uEc02B%&CBQa`GcikKk7^VyD4Zd5_aM zIYzI{38`bv*{ea@fy1|PWfSbies}!{_I*HC_q8i-zC15;rh*>i?RiADVhmD|2k2_4 zzQ1~EKK8kClTL31NX2nbSr0{UWC27q4~~71iX$@6966PFD1sv-AUJX=Yp$au$_~vb zZ5h$X-fD1`O4-+spj1djsX#}1MCG9fY7RuCM+({SgNB@Wf#z%rG|S(QkSh`AJvprA zGV&u?&Z&@d+?E_V?^>ktb3o<`smQyGU<^`GD$vRUDEvYlJn3LRgY;_vl{XLJa^VcS)%)ov%%*I&EJ zSvSleInvXdypdoh?}ngn0?_YMgMS zZy7+d&j8K#as3FkB|xxeC?nWjAr;#a&}>oHk6^0?1Y0|g$bAK6pe-C__u(!5%82oA z-7$h~B4uAcf_fkor2=hw&Q1o^Dq{PcF33~q8W8fwFVtFx$k%;z zMW@|NxQy6grwj6RA6)|q9K!$g3^iwiFJc`o%%eEcNBBky2K(?b;+?UHA3>rFIMRy| zn_qnPzT|;<4d)p;$3e@`o`=Su40N1Gevwq^h1;)Oa_rszbgd8UK4noj!<1xO@3ra3 zTPD2G3O7tW#jO+~?3}(k2N30?2;4;C8A04JBCR)+EInEwy(I*xcw(;_;bP=naD7$W z&@m51==oHfrlf>GSeuC6>$15RA%|W~``zlkRuSCIKu+v;W2w;Va}=(vfsV+l5js`} znyp>AKiA$_`}Svkvv*$RvF3`vim;QyuR61DkEPPLcl0!_TDlscWuxU^LgaVJ)HC#6 zSvA5*mEVc1=7AL^-q& z$uS&CjibJJG#F$GZM(+k*zo3!=Q1ZhS}<=%>&!6&X!hrzIjRANYJ`rQtPVPa=L9Tj zCwX|fH5LJFJg9X$1f`1Wc~Pp>&5n^=)93yv^x$|7Imdb^f_*p;9C3I=q=$}{m@lMa zP66>hb9cHEl*WLjRG=dsDN^GH?i`nZ=H6XNs!9aMC_wyYV;>?)Qbn3`4-%=EFK9{& z>0|41&Iw8Oxx(*x`dKRHhU-Ug{<(fV$ODp)hf3&qM0zNKJb+NCY)?w-pw=$zA++jd zD|haZMC#^k zET>9@j_wt00O+ASqI@gAD-(UG$@x`2gQ!HPt#not=LEUdy7LT!-rHl5wxZf{y6-Ep zbw|+Om5rQCbCkJC6-!c89*WR%R-MW`XdmVApCQ1{z5peb7A9Vgd=}>r z`P@*;-vBM&%@O)-C$v|T))26je%ndguGo(a6C>2G+Iu2+=r^j=D`N?jha#XkWpA&! zb5Uq0Rel5_<$z%c#ta300qZh7If^N?vLddF$?!ik3QQiSe0j5+zlB}9kx z;s1JuTErE4)FQ6$Bb++|)d)Q&q4hCFpd}-Z`1FRIhNwm;59HKdD&1*N9=hug^H2op zJT))YbE4qGQx-X1vB zGiXSJt^y<65GGfQP(7+`wR-LKBi}#b7B~OUeXc;a=b;FdM0e#(Cf9TNlQou?S@f;7 z*Do>NhN)+9jEzO&T{%Ud47jgWtRFm=W9dedDZlH*GjeW`@qH~FSOUk)g&o)!%jdaJr0sf2RK!x5@ z5tIZcWl6hq`I);Iam{b9ad`zEK#*QNDXZWCgyoxLqysu=lJoGh(F;fZ6$nqaq-xDl z%~oXuC2=|W76VpjE0xa+QaLN+{D`hbs3f3?4i+is(FAwsK})OM+Vb!S$WHr!CK@>f z4pM?%_MW-5ybOXxL)o+~^Bh6zqMS(wbUQu3gOaF^wdLXUXxS>KbF`PXz{5+Wl7M!I zAg%Z5b_ht4rxGiuRAC>4++8lwu9aFka1iTy*?}NBlmTm(c_NU0^`oUnaWAcmh<&%k z!=u&n5stj^%MmT9!a59m3|25btkwn;Wp7zjR&FSjAz~ga5BL1E23LEDBFbLr)i6E0 zwk?FGwSKhaQ9^*ndVSX;TAF)WeHYl=5AN-soM0}zwu)R^nGDI(9@Wf+|XJ^crA z=;6jC)VwI6NoANYF3AxN&7FL;f~AM8AQevtfz}Z+OS2j=EqOHeL};$FRHA}ZO#s1gLr`Hqy@$h4IO2xfYNTR#Q+==y4RU)|K3N&|O%ZP)fEXwzUx#L>W zJmR4U-j?(dzWipx~h-^Rs?yJY~?utix}Dp zG&Khu*{Tvj>jI&+Lhkt0v)=w^TuYYu{uM2pwU6yuIQXt2kS{$$Thx$y_`jY_tN%0~ zGu-)^8}gP~TkUFu_6(J+Xqt?0cCX#-&*@?hDtYUGZkK1)JNfU9$WhY$VnH1M8H+SO5S3 literal 0 HcmV?d00001 From e8f0f9cd47ca8401421ef14e9fba9351ee3ccfb2 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:35:00 +0200 Subject: [PATCH 011/115] Sync with PrusaSlicer-settings. Added Voron Switchwire. Based on https://github.com/slic3r/slic3r-profiles/pull/35. --- resources/profiles/Voron.idx | 1 + resources/profiles/Voron.ini | 300 ++++++++++++++++++++++++++++++++++- 2 files changed, 300 insertions(+), 1 deletion(-) diff --git a/resources/profiles/Voron.idx b/resources/profiles/Voron.idx index 7c519c08c..e59766e7f 100644 --- a/resources/profiles/Voron.idx +++ b/resources/profiles/Voron.idx @@ -1,4 +1,5 @@ min_slic3r_version = 2.6.0-alpha6 +1.0.3 Added Voron Switchwire. 1.0.2 Updated g-code flavor and travel accelerations. min_slic3r_version = 2.4.2 1.0.1 Added 350mm Voron v1 variant. Updated max print heights. Removed redundant v1 volcano nozzle variants. diff --git a/resources/profiles/Voron.ini b/resources/profiles/Voron.ini index 921fd375e..26dfcaab3 100644 --- a/resources/profiles/Voron.ini +++ b/resources/profiles/Voron.ini @@ -7,7 +7,7 @@ name = Voron # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.0.2 +config_version = 1.0.3 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/Voron/ @@ -106,6 +106,28 @@ bed_model = printbed-v0-120.stl bed_texture = bedtexture-v0-120.png default_materials = Basic PLA @VORON; Basic PLA VOLCANO @VORON; Basic PET @VORON; Basic PET VOLCANO @VORON; Basic ABS @VORON; Basic ABS VOLCANO @VORON +[printer_model:Voron_SW_afterburner] +name = Voron Switchwire +variants = 0.4; 0.25; 0.3; 0.5; 0.6; 0.8; volcano 0.6; volcano 0.8; volcano 1.0; volcano 1.2 +technology = FFF +family = Voron Switchwire Afterburner +bed_model = printbed-SW-MK52.stl +bed_texture = bedtexture-SW-250x210.png +bed_with_grid = 1 +default_materials = Basic PLA @VORON; Basic PLA VOLCANO @VORON; Basic PET @VORON; Basic PET VOLCANO @VORON; Basic ABS @VORON; Basic ABS VOLCANO @VORON +thumbnail = Voron_SW_thumbnail.png + +[printer_model:Voron_SW] +name = Voron Switchwire +variants = 0.4; 0.25; 0.3; 0.5; 0.6; 0.8; volcano 0.6; volcano 0.8; volcano 1.0; volcano 1.2 +technology = FFF +family = Voron Switchwire Mobius +bed_model = printbed-SW-MK52.stl +bed_texture = bedtexture-SW-250x210.png +bed_with_grid = 1 +default_materials = Basic PLA @VORON; Basic PLA VOLCANO @VORON; Basic PET @VORON; Basic PET VOLCANO @VORON; Basic ABS @VORON; Basic ABS VOLCANO @VORON +thumbnail = Voron_SW_thumbnail.png + # All presets starting with asterisk, for example *common*, are intermediate and they will # not make it into the user interface @@ -310,6 +332,18 @@ max_print_height = 120 printer_model = Voron_v0_120 printer_notes = Unoffical profile.\nPRINTER_HAS_BOWDEN\nE3DV6 +[printer:*Voron_Switchwire*] +inherits = *common* +bed_shape = 0x0,250x0,250x210,0x210 +max_print_height = 240 +printer_model = Voron_SW +printer_notes = PRINTER_HAS_BOWDEN\nSTU\nE3DV6 + +[printer:*Voron_Switchwire_afterburner*] +inherits = *Voron_Switchwire*; *afterburner* +printer_model = Voron_SW_afterburner +printer_notes = STU\nE3DV6 + [printer:Voron_v2_250 0.25 nozzle] inherits = *Voron_v2_250*; *0.25nozzle* @@ -658,6 +692,89 @@ printer_variant = volcano 1.2 printer_notes = Unoffical profile.\nPRINTER_HAS_BOWDEN\nVOLCANO default_filament_profile = Basic PLA VOLCANO @VORON +[printer:Voron_Switchwire 0.25 nozzle] +inherits = *Voron_Switchwire*; *0.25nozzle* + +[printer:Voron_Switchwire 0.3 nozzle] +inherits = *Voron_Switchwire*; *0.3nozzle* + +[printer:Voron_Switchwire 0.4 nozzle] +inherits = *Voron_Switchwire*; *0.4nozzle* + +[printer:Voron_Switchwire 0.5 nozzle] +inherits = *Voron_Switchwire*; *0.5nozzle* + +[printer:Voron_Switchwire 0.6 nozzle] +inherits = *Voron_Switchwire*; *0.6nozzle* + +[printer:Voron_Switchwire 0.8 nozzle] +inherits = *Voron_Switchwire*; *0.8nozzle* + +[printer:Voron_Switchwire 0.6 volcano] +inherits = *Voron_Switchwire*; *0.6nozzle*; *volcano* +printer_variant = volcano 0.6 +printer_notes = PRINTER_HAS_BOWDEN\nVOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire 0.8 volcano] +inherits = *Voron_Switchwire*; *0.8nozzle*; *volcano* +printer_variant = volcano 0.8 +printer_notes = PRINTER_HAS_BOWDEN\nVOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire 1.0 volcano] +inherits = *Voron_Switchwire*; *1.0nozzle*; *volcano* +printer_variant = volcano 1.0 +printer_notes = PRINTER_HAS_BOWDEN\nVOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire 1.2 volcano] +inherits = *Voron_Switchwire*; *1.2nozzle*; *volcano* +printer_variant = volcano 1.2 +printer_notes = PRINTER_HAS_BOWDEN\nVOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire_afterburner 0.25 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.25nozzle* + +[printer:Voron_Switchwire_afterburner 0.3 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.3nozzle* + +[printer:Voron_Switchwire_afterburner 0.4 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.4nozzle* + +[printer:Voron_Switchwire_afterburner 0.5 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.5nozzle* + +[printer:Voron_Switchwire_afterburner 0.6 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.6nozzle* + +[printer:Voron_Switchwire_afterburner 0.8 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.8nozzle* + +[printer:Voron_Switchwire_afterburner volcano 0.6 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.6nozzle*; *volcano_afterburner* +printer_variant = volcano 0.6 +printer_notes = VOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire_afterburner volcano 0.8 nozzle] +inherits = *Voron_Switchwire_afterburner*; *0.8nozzle*; *volcano_afterburner* +printer_variant = volcano 0.8 +printer_notes = VOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire_afterburner volcano 1.0 nozzle] +inherits = *Voron_Switchwire_afterburner*; *1.0nozzle*; *volcano_afterburner* +printer_variant = volcano 1.0 +printer_notes = VOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON + +[printer:Voron_Switchwire_afterburner volcano 1.2 nozzle] +inherits = *Voron_Switchwire_afterburner*; *1.2nozzle*; *volcano_afterburner* +printer_variant = volcano 1.2 +printer_notes = VOLCANO +default_filament_profile = Basic PLA VOLCANO @VORON # Common print preset, mostly derived from MK2 single material with a 0.4mm nozzle. # All other print presets will derive from the *common* print preset. @@ -1022,6 +1139,22 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.05mm*; *0.5nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==0.5 +[print:0.05mm 0.25nozzle SW] +inherits = *0.05mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.05mm 0.3nozzle SW] +inherits = *0.05mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.05mm 0.4nozzle SW] +inherits = *0.05mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.05mm 0.5nozzle SW] +inherits = *0.05mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + [print:0.10mm 0.25nozzle V2] inherits = *0.10mm*; *0.25nozzle* compatible_printers_condition = printer_model=~/.*Voron_v2.*/ and nozzle_diameter[0]==0.25 @@ -1094,6 +1227,30 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.10mm*; *0.8nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==0.8 +[print:0.10mm 0.25nozzle SW] +inherits = *0.10mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.10mm 0.3nozzle SW] +inherits = *0.10mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.10mm 0.4nozzle SW] +inherits = *0.10mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.10mm 0.5nozzle SW] +inherits = *0.10mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + +[print:0.10mm 0.6nozzle SW] +inherits = *0.10mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.6 + +[print:0.10mm 0.8nozzle SW] +inherits = *0.10mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + [print:0.15mm 0.25nozzle V2] inherits = *0.15mm*; *0.25nozzle* compatible_printers_condition = printer_model=~/.*Voron_v2.*/ and nozzle_diameter[0]==0.25 @@ -1182,6 +1339,37 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.15mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.15mm 0.25nozzle SW] +inherits = *0.15mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.15mm 0.3nozzle SW] +inherits = *0.15mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.15mm 0.4nozzle SW] +inherits = *0.15mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.15mm 0.5nozzle SW] +inherits = *0.15mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + +[print:0.15mm 0.6nozzle SW] +inherits = *0.15mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.6 + +[print:0.15mm 0.8nozzle SW] +inherits = *0.15mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + +[print:0.15mm 1.0nozzle SW] +inherits = *0.15mm*; *1.0nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.0 + +[print:0.15mm 1.2nozzle SW] +inherits = *0.15mm*; *1.2nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 [print:0.2mm 0.3nozzle V2] inherits = *0.2mm*; *0.3nozzle* @@ -1263,6 +1451,37 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.2mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.2mm 0.25nozzle SW] +inherits = *0.2mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.2mm 0.3nozzle SW] +inherits = *0.2mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.2mm 0.4nozzle SW] +inherits = *0.2mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.2mm 0.5nozzle SW] +inherits = *0.2mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + +[print:0.2mm 0.6nozzle SW] +inherits = *0.2mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.6 + +[print:0.2mm 0.8nozzle SW] +inherits = *0.2mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + +[print:0.2mm 1.0nozzle SW] +inherits = *0.2mm*; *1.0nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.0 + +[print:0.2mm 1.2nozzle SW] +inherits = *0.2mm*; *1.2nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 [print:0.3mm 0.4nozzle V2] inherits = *0.3mm*; *0.4nozzle* @@ -1336,6 +1555,37 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.3mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.3mm 0.25nozzle SW] +inherits = *0.3mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.3mm 0.3nozzle SW] +inherits = *0.3mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.3mm 0.4nozzle SW] +inherits = *0.3mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.3mm 0.5nozzle SW] +inherits = *0.3mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + +[print:0.3mm 0.6nozzle SW] +inherits = *0.3mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.6 + +[print:0.3mm 0.8nozzle SW] +inherits = *0.3mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + +[print:0.3mm 1.0nozzle SW] +inherits = *0.3mm*; *1.0nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.0 + +[print:0.3mm 1.2nozzle SW] +inherits = *0.3mm*; *1.2nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 [print:0.4mm 0.6nozzle V2] inherits = *0.4mm*; *0.6nozzle* @@ -1393,6 +1643,38 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.4mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.4mm 0.25nozzle SW] +inherits = *0.4mm*; *0.25nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.25 + +[print:0.4mm 0.3nozzle SW] +inherits = *0.4mm*; *0.3nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.3 + +[print:0.4mm 0.4nozzle SW] +inherits = *0.4mm*; *0.4nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.4 + +[print:0.4mm 0.5nozzle SW] +inherits = *0.4mm*; *0.5nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.5 + +[print:0.4mm 0.6nozzle SW] +inherits = *0.4mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.6 + +[print:0.4mm 0.8nozzle SW] +inherits = *0.4mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + +[print:0.4mm 1.0nozzle SW] +inherits = *0.4mm*; *1.0nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.0 + +[print:0.4mm 1.2nozzle SW] +inherits = *0.4mm*; *1.2nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 + [print:0.6mm 0.8nozzle V2] inherits = *0.6mm*; *0.8nozzle* compatible_printers_condition = printer_model=~/.*Voron_v2.*/ and nozzle_diameter[0]==0.8 @@ -1429,6 +1711,18 @@ compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diamete inherits = *0.6mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.6mm 0.8nozzle SW] +inherits = *0.6mm*; *0.6nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==0.8 + +[print:0.6mm 1.0nozzle SW] +inherits = *0.6mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.0 + +[print:0.6mm 1.2nozzle SW] +inherits = *0.6mm*; *0.8nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 + [print:0.8mm 1.2nozzle V2] inherits = *0.8mm*; *1.2nozzle* compatible_printers_condition = printer_model=~/.*Voron_v2.*/ and nozzle_diameter[0]==1.2 @@ -1441,6 +1735,10 @@ compatible_printers_condition = printer_model=~/.*Voron_v1.*/ and nozzle_diamete inherits = *0.8mm*; *1.2nozzle*; *zero_toolhead* compatible_printers_condition = printer_model=~/.*Voron_v0.*/ and nozzle_diameter[0]==1.2 +[print:0.8mm 1.2nozzle SW] +inherits = *0.8mm*; *1.2nozzle* +compatible_printers_condition = printer_model=~/.*Voron_SW.*/ and nozzle_diameter[0]==1.2 + [filament:*common*] cooling = 1 From 54db40eae27cfdfc337e3a95be97695b6d10f689 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 13 Apr 2023 17:27:37 +0200 Subject: [PATCH 012/115] Improve bridging lines cut on lightning infill --- src/libslic3r/PrintObject.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 5ee3a604d..0c3bf87b9 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1603,21 +1603,18 @@ void PrintObject::bridge_over_infill() int layer_index, Polygons new_polys, const LayerRegion *region, - double bridge_angle, - bool supported_by_lightning) + double bridge_angle) : original_surface(original_surface) , layer_index(layer_index) , new_polys(new_polys) , region(region) , bridge_angle(bridge_angle) - , supported_by_lightning(supported_by_lightning) {} const Surface *original_surface; int layer_index; Polygons new_polys; const LayerRegion *region; double bridge_angle; - bool supported_by_lightning; }; std::map> surfaces_by_layer; @@ -1677,7 +1674,7 @@ void PrintObject::bridge_over_infill() } } worth_bridging = intersection(closing(worth_bridging, SCALED_EPSILON), s->expolygon); - candidate_surfaces.push_back(CandidateSurface(s, lidx, worth_bridging, region, 0, contains_only_lightning)); + candidate_surfaces.push_back(CandidateSurface(s, lidx, worth_bridging, region, 0)); #ifdef DEBUG_BRIDGE_OVER_INFILL debug_draw(std::to_string(lidx) + "_candidate_surface_" + std::to_string(area(s->expolygon)), @@ -2135,6 +2132,7 @@ void PrintObject::bridge_over_infill() deep_infill_area = expand(deep_infill_area, spacing * 1.5); // Now gather expansion polygons - internal infill on current layer, from which we can cut off anchors + Polygons lightning_area; Polygons expansion_area; Polygons total_fill_area; for (const LayerRegion *region : layer->regions()) { @@ -2142,6 +2140,10 @@ void PrintObject::bridge_over_infill() expansion_area.insert(expansion_area.end(), internal_polys.begin(), internal_polys.end()); Polygons fill_polys = to_polygons(region->fill_expolygons()); total_fill_area.insert(total_fill_area.end(), fill_polys.begin(), fill_polys.end()); + if (region->region().config().fill_pattern == ipLightning) { + Polygons l = to_polygons(region->fill_surfaces().filter_by_type(stInternal)); + lightning_area.insert(lightning_area.end(), l.begin(), l.end()); + } } total_fill_area = closing(total_fill_area, SCALED_EPSILON); expansion_area = closing(expansion_area, SCALED_EPSILON); @@ -2196,7 +2198,7 @@ void PrintObject::bridge_over_infill() } boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end()); - if (candidate.supported_by_lightning) { + if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) { boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10))); } Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle); @@ -2229,7 +2231,7 @@ void PrintObject::bridge_over_infill() #endif expanded_surfaces.push_back(CandidateSurface(candidate.original_surface, candidate.layer_index, bridging_area, - candidate.region, bridging_angle, candidate.supported_by_lightning)); + candidate.region, bridging_angle)); } surfaces_by_layer[lidx].swap(expanded_surfaces); expanded_surfaces.clear(); From bd301d2a8599786532b9b43a645989c02ca4f6fe Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 13 Apr 2023 16:08:05 +0200 Subject: [PATCH 013/115] Elephant foot compensation: Refactored / simplified, fixed an error for variable ExPolygon expansion (not used in production code yet), fixed asserts when expanding a hole produces a hole in hole, which is a valid situation. --- src/libslic3r/ClipperUtils.cpp | 266 +++++++++++---------- src/libslic3r/ElephantFootCompensation.cpp | 4 +- 2 files changed, 139 insertions(+), 131 deletions(-) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index ed76fc66a..2869d0c87 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -1,6 +1,7 @@ #include "ClipperUtils.hpp" #include "Geometry.hpp" #include "ShortestPath.hpp" +#include "Utils.hpp" // #define CLIPPER_UTILS_DEBUG @@ -1167,34 +1168,45 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v return out; } -Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +static void variable_offset_inner_raw(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit, ClipperLib::Paths &contours, ClipperLib::Paths &holes) { #ifndef NDEBUG - // Verify that the deltas are all non positive. - for (const std::vector &ds : deltas) - for (float delta : ds) - assert(delta <= 0.); - assert(expoly.holes.size() + 1 == deltas.size()); + // Verify that the deltas are all non positive. + for (const std::vector &ds : deltas) + for (float delta : ds) + assert(delta <= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); + assert(ClipperLib::Area(expoly.contour.points) > 0.); + for (auto &h : expoly.holes) + assert(ClipperLib::Area(h.points) < 0.); #endif /* NDEBUG */ - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_inner_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftNegative, true); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); + // 1) Offset the outer contour. + contours = fix_after_inner_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftNegative, true); +#ifndef NDEBUG + // Shrinking a contour may split it into pieces, but never create a new hole inside the contour. + for (auto &c : contours) + assert(ClipperLib::Area(c) > 0.); #endif /* NDEBUG */ - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, false)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); + // 2) Offset the holes one by one, collect the results. + holes.reserve(expoly.holes.size()); + for (const Polygon &hole : expoly.holes) + append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, false)); +#ifndef NDEBUG + // Offsetting a hole curve of a C shape may close the C into a ring with a new hole inside, thus creating a hole inside a hole shape, thus a hole will be created with negative area + // and the following test will fail. +// for (auto &c : holes) +// assert(ClipperLib::Area(c) > 0.); #endif /* NDEBUG */ +} - // 3) Subtract holes from the contours. +Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ + ClipperLib::Paths contours, holes; + variable_offset_inner_raw(expoly, deltas, miter_limit, contours, holes); + + // Subtract holes from the contours. ClipperLib::Paths output; if (holes.empty()) output = std::move(contours); @@ -1202,6 +1214,8 @@ Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) +{ + ClipperLib::Paths contours, holes; + variable_offset_inner_raw(expoly, deltas, miter_limit, contours, holes); + + // Subtract holes from the contours. + ExPolygons output; + if (holes.empty()) { + output.reserve(contours.size()); + // Shrinking a CCW contour may only produce more CCW contours, but never new holes. + for (ClipperLib::Path &path : contours) + output.emplace_back(std::move(path)); + } else { + ClipperLib::Clipper clipper; + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + // Holes may contain holes in holes produced by expanding a C hole shape. + // The situation is processed correctly by Clipper diff operation, producing concentric expolygons. + clipper.AddPaths(holes, ClipperLib::ptClip, true); + ClipperLib::PolyTree polytree; + clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + output = PolyTreeToExPolygons(std::move(polytree)); + } + + return output; +} + +static void variable_offset_outer_raw(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit, ClipperLib::Paths &contours, ClipperLib::Paths &holes) +{ +#ifndef NDEBUG + // Verify that the deltas are all non positive. + for (const std::vector &ds : deltas) + for (float delta : ds) + assert(delta >= 0.); + assert(expoly.holes.size() + 1 == deltas.size()); + assert(ClipperLib::Area(expoly.contour.points) > 0.); + for (auto &h : expoly.holes) + assert(ClipperLib::Area(h.points) < 0.); +#endif /* NDEBUG */ + + // 1) Offset the outer contour. + contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); + // Inflating a contour must not remove it. + assert(contours.size() >= 1); +#ifndef NDEBUG + // Offsetting a positive curve of a C shape may close the C into a ring with hole shape, thus a hole will be created with negative area + // and the following test will fail. +// for (auto &c : contours) +// assert(ClipperLib::Area(c) > 0.); +#endif /* NDEBUG */ + + // 2) Offset the holes one by one, collect the results. + holes.reserve(expoly.holes.size()); + for (const Polygon& hole : expoly.holes) + append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); +#ifndef NDEBUG + // Shrinking a hole may split it into pieces, but never create a new hole inside a hole. + for (auto &c : holes) + assert(ClipperLib::Area(c) > 0.); +#endif /* NDEBUG */ +} + Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { -#ifndef NDEBUG - // Verify that the deltas are all non positive. -for (const std::vector& ds : deltas) - for (float delta : ds) - assert(delta >= 0.); - assert(expoly.holes.size() + 1 == deltas.size()); -#endif /* NDEBUG */ + ClipperLib::Paths contours, holes; + variable_offset_outer_raw(expoly, deltas, miter_limit, contours, holes); - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ + // Subtract holes from the contours. + ClipperLib::Paths output; + if (holes.empty()) + output = std::move(contours); + else { + //FIXME the difference is not needed as the holes may never intersect with other holes. + ClipperLib::Clipper clipper; + clipper.Clear(); + clipper.AddPaths(contours, ClipperLib::ptSubject, true); + clipper.AddPaths(holes, ClipperLib::ptClip, true); + clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + } - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ClipperLib::Paths output; - if (holes.empty()) - output = std::move(contours); - else { - ClipperLib::Clipper clipper; - clipper.Clear(); - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - clipper.Execute(ClipperLib::ctDifference, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - } - - return to_polygons(std::move(output)); + return to_polygons(std::move(output)); } ExPolygons variable_offset_outer_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { -#ifndef NDEBUG - // Verify that the deltas are all non positive. -for (const std::vector& ds : deltas) - for (float delta : ds) - assert(delta >= 0.); - assert(expoly.holes.size() + 1 == deltas.size()); -#endif /* NDEBUG */ + ClipperLib::Paths contours, holes; + variable_offset_outer_raw(expoly, deltas, miter_limit, contours, holes); - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_outer_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftPositive, false); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_inner_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftPositive, true)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. + // Subtract holes from the contours. ExPolygons output; if (holes.empty()) { - output.reserve(contours.size()); - for (ClipperLib::Path &path : contours) - output.emplace_back(std::move(path)); - } else { - ClipperLib::Clipper clipper; - clipper.AddPaths(contours, ClipperLib::ptSubject, true); - clipper.AddPaths(holes, ClipperLib::ptClip, true); - ClipperLib::PolyTree polytree; - clipper.Execute(ClipperLib::ctDifference, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - output = PolyTreeToExPolygons(std::move(polytree)); - } - - return output; -} - - -ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) -{ -#ifndef NDEBUG - // Verify that the deltas are all non positive. - for (const std::vector& ds : deltas) - for (float delta : ds) - assert(delta <= 0.); - assert(expoly.holes.size() + 1 == deltas.size()); -#endif /* NDEBUG */ - - // 1) Offset the outer contour. - ClipperLib::Paths contours = fix_after_inner_offset(mittered_offset_path_scaled(expoly.contour.points, deltas.front(), miter_limit), ClipperLib::pftNegative, true); -#ifndef NDEBUG - for (auto &c : contours) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 2) Offset the holes one by one, collect the results. - ClipperLib::Paths holes; - holes.reserve(expoly.holes.size()); - for (const Polygon& hole : expoly.holes) - append(holes, fix_after_outer_offset(mittered_offset_path_scaled(hole.points, deltas[1 + &hole - expoly.holes.data()], miter_limit), ClipperLib::pftNegative, false)); -#ifndef NDEBUG - for (auto &c : holes) - assert(ClipperLib::Area(c) > 0.); -#endif /* NDEBUG */ - - // 3) Subtract holes from the contours. - ExPolygons output; - if (holes.empty()) { - output.reserve(contours.size()); - for (ClipperLib::Path &path : contours) - output.emplace_back(std::move(path)); + output.reserve(1); + if (contours.size() > 1) { + // One expolygon with holes created by closing a C shape. Which is which? + output.push_back({}); + ExPolygon &out = output.back(); + out.holes.reserve(contours.size() - 1); + for (ClipperLib::Path &path : contours) { + if (ClipperLib::Area(path) > 0) { + // Only one contour with positive area is expected to be created by an outer offset of an ExPolygon. + assert(out.contour.empty()); + out.contour.points = std::move(path); + } else + out.holes.push_back(Polygon{ std::move(path) }); + } + } else { + // Single contour must be CCW. + assert(contours.size() == 1); + assert(ClipperLib::Area(contours.front()) > 0); + output.push_back(ExPolygon{ std::move(contours.front()) }); + } } else { + //FIXME the difference is not needed as the holes may never intersect with other holes. ClipperLib::Clipper clipper; + // Contours may have holes if they were created by closing a C shape. clipper.AddPaths(contours, ClipperLib::ptSubject, true); clipper.AddPaths(holes, ClipperLib::ptClip, true); ClipperLib::PolyTree polytree; @@ -1339,6 +1344,7 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector Date: Thu, 13 Apr 2023 16:09:29 +0200 Subject: [PATCH 014/115] Tests for duplicate points in Polygons / ExPolygons were reworked to use ankerl::unordered_dense hash map. Now the tests are roughly 1/4 faster than before. --- src/libslic3r/ExPolygon.cpp | 32 +++++++++++++++++++++++++------- src/libslic3r/Polygon.cpp | 28 ++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index dc991e46d..b6d12a602 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -10,6 +10,8 @@ #include #include +#include + namespace Slic3r { void ExPolygon::scale(double factor) @@ -416,20 +418,36 @@ bool has_duplicate_points(const ExPolygons &expolys) { #if 1 // Check globally. - size_t cnt = 0; - for (const ExPolygon &expoly : expolys) { - cnt += expoly.contour.points.size(); - for (const Polygon &hole : expoly.holes) - cnt += hole.points.size(); - } +#if 0 + // Detect duplicates by sorting with quicksort. It is quite fast, but ankerl::unordered_dense is around 1/4 faster. std::vector allpts; - allpts.reserve(cnt); + allpts.reserve(count_points(expolys)); for (const ExPolygon &expoly : expolys) { allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); for (const Polygon &hole : expoly.holes) allpts.insert(allpts.end(), hole.points.begin(), hole.points.end()); } return has_duplicate_points(std::move(allpts)); +#else + // Detect duplicates by inserting into an ankerl::unordered_dense hash set, which is is around 1/4 faster than qsort. + struct PointHash { + uint64_t operator()(const Point &p) const noexcept { + uint64_t h; + static_assert(sizeof(h) == sizeof(p)); + memcpy(&h, &p, sizeof(p)); + return ankerl::unordered_dense::detail::wyhash::hash(h); + } + }; + ankerl::unordered_dense::set allpts; + allpts.reserve(count_points(expolys)); + for (const ExPolygon &expoly : expolys) + for (size_t icontour = 0; icontour < expoly.num_contours(); ++ icontour) + for (const Point &pt : expoly.contour_or_hole(icontour).points) + if (! allpts.insert(pt).second) + // Duplicate point was discovered. + return true; + return false; +#endif #else // Check per contour. for (const ExPolygon &expoly : expolys) diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index d342f3d91..3be36984c 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -4,6 +4,8 @@ #include "Polygon.hpp" #include "Polyline.hpp" +#include + namespace Slic3r { double Polygon::length() const @@ -400,14 +402,32 @@ bool has_duplicate_points(const Polygons &polys) { #if 1 // Check globally. - size_t cnt = 0; - for (const Polygon &poly : polys) - cnt += poly.points.size(); +#if 0 + // Detect duplicates by sorting with quicksort. It is quite fast, but ankerl::unordered_dense is around 1/4 faster. std::vector allpts; - allpts.reserve(cnt); + allpts.reserve(count_points(polys)); for (const Polygon &poly : polys) allpts.insert(allpts.end(), poly.points.begin(), poly.points.end()); return has_duplicate_points(std::move(allpts)); +#else + // Detect duplicates by inserting into an ankerl::unordered_dense hash set, which is is around 1/4 faster than qsort. + struct PointHash { + uint64_t operator()(const Point &p) const noexcept { + uint64_t h; + static_assert(sizeof(h) == sizeof(p)); + memcpy(&h, &p, sizeof(p)); + return ankerl::unordered_dense::detail::wyhash::hash(h); + } + }; + ankerl::unordered_dense::set allpts; + allpts.reserve(count_points(polys)); + for (const Polygon &poly : polys) + for (const Point &pt : poly.points) + if (! allpts.insert(pt).second) + // Duplicate point was discovered. + return true; + return false; +#endif #else // Check per contour. for (const Polygon &poly : polys) From adabaccc9e76a24ace5091fd36b0d9f14bafc2d9 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 14 Apr 2023 08:36:47 +0200 Subject: [PATCH 015/115] TriangleMeshSlicer: Added tests for checking for non-manifold sets of contours created by the slicing algorithm. Currently these tests are disabled as it is known that such situations may appear. --- src/libslic3r/TriangleMeshSlicer.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 7faa79435..329c1c4ca 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1911,6 +1911,16 @@ std::vector slice_mesh_ex( this_mode == MeshSlicingParams::SlicingMode::EvenOdd ? ClipperLib::pftEvenOdd : this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour ? ClipperLib::pftPositive : ClipperLib::pftNonZero, &expolygons); +#if 0 +//#ifndef _NDEBUG + for (const ExPolygon &ex : expolygons) { + assert(! has_duplicate_points(ex.contour)); + for (const Polygon &hole : ex.holes) + assert(! has_duplicate_points(hole)); + assert(! has_duplicate_points(ex)); + } + assert(!has_duplicate_points(expolygons)); +#endif // _NDEBUG //FIXME simplify if (this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour) keep_largest_contour_only(expolygons); @@ -1921,6 +1931,16 @@ std::vector slice_mesh_ex( append(simplified, ex.simplify(resolution)); expolygons = std::move(simplified); } +#if 0 +//#ifndef _NDEBUG + for (const ExPolygon &ex : expolygons) { + assert(! has_duplicate_points(ex.contour)); + for (const Polygon &hole : ex.holes) + assert(! has_duplicate_points(hole)); + assert(! has_duplicate_points(ex)); + } + assert(! has_duplicate_points(expolygons)); +#endif // _NDEBUG } }); // BOOST_LOG_TRIVIAL(debug) << "slice_mesh make_expolygons in parallel - end"; From 06403eef65969844d8a09342afbbae74839a424c Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 14 Apr 2023 09:21:39 +0200 Subject: [PATCH 016/115] SPE-1461 improved error reporting: if the last layer exceeds max print height while the object itself fits, a specific error report is given: "While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height." Also the name of the object violating print height is reported in the error message. --- src/libslic3r/Print.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 14e77e03f..b68d04b2b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -518,8 +518,12 @@ std::string Print::validate(std::string* warning) const //FIXME It is quite expensive to generate object layers just to get the print height! if (auto layers = generate_object_layers(print_object.slicing_parameters(), layer_height_profile(print_object_idx)); ! layers.empty() && layers.back() > this->config().max_print_height + EPSILON) { - return _u8L("The print is taller than the maximum allowed height. You might want to reduce the size of your model" - " or change current print settings and retry."); + return + // Test whether the last slicing plane is below or above the print volume. + 0.5 * (layers[layers.size() - 2] + layers.back()) > this->config().max_print_height + EPSILON ? + format(_u8L("The object %1% exceeds the maximum build volume height."), print_object.model_object()->name) : + format(_u8L("While the object %1% itself fits the build volume, its last layer exceeds the maximum build volume height."), print_object.model_object()->name) + + " " + _u8L("You might want to reduce the size of your model or change current print settings and retry."); } } From f6da852353e1e8efbeb1c50335678dffb5ee3051 Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Mon, 3 Apr 2023 10:30:55 +0200 Subject: [PATCH 017/115] Fix of #10210 (crash when using mainsail print host) and some related UI fixes ('OctoPrint'->'Mainsail/Fluidd') --- src/libslic3r/PrintConfig.hpp | 2 +- src/slic3r/Utils/OctoPrint.cpp | 20 ++++++++++++++++---- src/slic3r/Utils/OctoPrint.hpp | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 06f0472ae..b9ca95a15 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -44,7 +44,7 @@ enum class MachineLimitsUsage { }; enum PrintHostType { - htPrusaLink, htPrusaConnect, htOctoPrint, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS, htMainSail + htPrusaLink, htPrusaConnect, htOctoPrint, htMainSail, htDuet, htFlashAir, htAstroBox, htRepetier, htMKS }; enum AuthorizationType { diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index 3fc9ab62e..d78e63185 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -203,7 +203,7 @@ bool OctoPrint::test_with_resolved_ip(wxString &msg) const const auto text = ptree.get_optional("text"); res = validate_version_text(text); if (!res) { - msg = GUI::format_wxstr(_L("Mismatched type of print host: %s"), (text ? *text : "OctoPrint")); + msg = GUI::format_wxstr(_L("Mismatched type of print host: %s"), (text ? *text : name)); } } catch (const std::exception&) { @@ -252,7 +252,7 @@ bool OctoPrint::test(wxString& msg) const const auto text = ptree.get_optional("text"); res = validate_version_text(text); if (! res) { - msg = GUI::format_wxstr(_L("Mismatched type of print host: %s"), (text ? *text : "OctoPrint")); + msg = GUI::format_wxstr(_L("Mismatched type of print host: %s"), (text ? *text : name)); } } catch (const std::exception &) { @@ -396,7 +396,7 @@ bool OctoPrint::upload_inner_with_resolved_ip(PrintHostUpload upload_data, Progr prorgess_fn(std::move(progress), cancel); if (cancel) { // Upload was canceled - BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled"; + BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled"; result = false; } }) @@ -473,7 +473,7 @@ bool OctoPrint::upload_inner_with_host(PrintHostUpload upload_data, ProgressFn p prorgess_fn(std::move(progress), cancel); if (cancel) { // Upload was canceled - BOOST_LOG_TRIVIAL(info) << "Octoprint: Upload canceled"; + BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled"; res = false; } }) @@ -1127,4 +1127,16 @@ wxString PrusaConnect::get_test_failed_msg(wxString& msg) const return GUI::format_wxstr("%s: %s", _L("Could not connect to Prusa Connect"), msg); } + + +wxString Mainsail::get_test_ok_msg() const +{ + return _(L("Connection to Mainsail/Fluidd works correctly.")); +} + +wxString Mainsail::get_test_failed_msg(wxString& msg) const +{ + return GUI::format_wxstr("%s: %s", _L("Could not connect to MainSail/Fluidd"), msg); +} + } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index 2daeab73f..d12e28a5b 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -125,6 +125,8 @@ public: ~Mainsail() override = default; const char* get_name() const override { return "Mainsail/Fluidd"; } + wxString get_test_ok_msg() const override; + wxString get_test_failed_msg(wxString& msg) const override; }; class SL1Host : public PrusaLink From 4e52d3c56db8bd3422f9bf3a17f91cb8892f440a Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 5 Apr 2023 15:11:38 +0200 Subject: [PATCH 018/115] Mainsail API implementation --- src/slic3r/CMakeLists.txt | 2 + src/slic3r/Utils/Mainsail.cpp | 184 +++++++++++++++++++++++++++++++++ src/slic3r/Utils/Mainsail.hpp | 64 ++++++++++++ src/slic3r/Utils/OctoPrint.cpp | 13 --- src/slic3r/Utils/OctoPrint.hpp | 12 --- src/slic3r/Utils/PrintHost.cpp | 1 + 6 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 src/slic3r/Utils/Mainsail.cpp create mode 100644 src/slic3r/Utils/Mainsail.hpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index db8cefa99..35e05f506 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -250,6 +250,8 @@ set(SLIC3R_GUI_SOURCES Utils/Http.hpp Utils/FixModelByWin10.cpp Utils/FixModelByWin10.hpp + Utils/Mainsail.cpp + Utils/Mainsail.hpp Utils/OctoPrint.cpp Utils/OctoPrint.hpp Utils/Duet.cpp diff --git a/src/slic3r/Utils/Mainsail.cpp b/src/slic3r/Utils/Mainsail.cpp new file mode 100644 index 000000000..0953096da --- /dev/null +++ b/src/slic3r/Utils/Mainsail.cpp @@ -0,0 +1,184 @@ +#include "Mainsail.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "slic3r/GUI/GUI.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/format.hpp" +#include "Http.hpp" + +namespace fs = boost::filesystem; +namespace pt = boost::property_tree; +namespace Slic3r { + +Mainsail::Mainsail(DynamicPrintConfig *config) : + m_host(config->opt_string("print_host")), + m_apikey(config->opt_string("printhost_apikey")), + m_cafile(config->opt_string("printhost_cafile")), + m_ssl_revoke_best_effort(config->opt_bool("printhost_ssl_ignore_revoke")) +{} + +const char* Mainsail::get_name() const { return "Mainsail"; } + +wxString Mainsail::get_test_ok_msg () const +{ + return _(L("Connection to Mainsail works correctly.")); +} + +wxString Mainsail::get_test_failed_msg (wxString &msg) const +{ + return GUI::format_wxstr("%s: %s" + , _L("Could not connect to Mainsail") + , msg); +} + +bool Mainsail::test(wxString& msg) const +{ + // GET /server/info + + // Since the request is performed synchronously here, + // it is ok to refer to `msg` from within the closure + const char* name = get_name(); + + bool res = true; + auto url = make_url("server/info"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Get version at: %2%") % name % url; + + auto http = Http::get(std::move(url)); + set_auth(http); + http.on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error getting version: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + res = false; + msg = format_error(body, error, status); + }) + .on_complete([&, this](std::string body, unsigned) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: Got server/info: %2%") % name % body; + + try { + // All successful HTTP requests will return a json encoded object in the form of : + // {result: } + std::stringstream ss(body); + pt::ptree ptree; + pt::read_json(ss, ptree); + if (ptree.front().first != "result") { + msg = "Could not parse server response"; + res = false; + return; + } + if (!ptree.front().second.get_optional("moonraker_version")) { + msg = "Could not parse server response"; + res = false; + return; + } + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Got version: %2%") % name % ptree.front().second.get_optional("moonraker_version"); + } catch (const std::exception&) { + res = false; + msg = "Could not parse server response"; + } + }) + .perform_sync(); + + return res; +} + +bool Mainsail::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const +{ + // POST /server/files/upload + + const char* name = get_name(); + const auto upload_filename = upload_data.upload_path.filename(); + const auto upload_parent_path = upload_data.upload_path.parent_path(); + + // If test fails, test_msg_or_host_ip contains the error message. + wxString test_msg_or_host_ip; + if (!test(test_msg_or_host_ip)) { + error_fn(std::move(test_msg_or_host_ip)); + return false; + } + + std::string url; + bool res = true; + + url = make_url("server/files/upload"); + + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") + % name + % upload_data.source_path + % url + % upload_filename.string() + % upload_parent_path.string() + % (upload_data.post_action == PrintHostPostUploadAction::StartPrint ? "true" : "false"); + /* + The file must be uploaded in the request's body multipart/form-data (ie: ). The following arguments may also be added to the form-data: + root: The root location in which to upload the file.Currently this may be gcodes or config.If not specified the default is gcodes. + path : This argument may contain a path(relative to the root) indicating a subdirectory to which the file is written.If a path is present the server will attempt to create any subdirectories that do not exist. + checksum : A SHA256 hex digest calculated by the client for the uploaded file.If this argument is supplied the server will compare it to its own checksum calculation after the upload has completed.A checksum mismatch will result in a 422 error. + Arguments available only for the gcodes root : + print: If set to "true", Klippy will attempt to start the print after uploading.Note that this value should be a string type, not boolean.This provides compatibility with OctoPrint's upload API. + */ + auto http = Http::post(std::move(url)); + set_auth(http); + + http.form_add("root", "gcodes"); + if (!upload_parent_path.empty()) + http.form_add("path", upload_parent_path.string()); + if (upload_data.post_action == PrintHostPostUploadAction::StartPrint) + http.form_add("print", "true"); + + http.form_add_file("file", upload_data.source_path.string(), upload_filename.string()) + .on_complete([&](std::string body, unsigned status) { + BOOST_LOG_TRIVIAL(debug) << boost::format("%1%: File uploaded: HTTP %2%: %3%") % name % status % body; + }) + .on_error([&](std::string body, std::string error, unsigned status) { + BOOST_LOG_TRIVIAL(error) << boost::format("%1%: Error uploading file: %2%, HTTP %3%, body: `%4%`") % name % error % status % body; + error_fn(format_error(body, error, status)); + res = false; + }) + .on_progress([&](Http::Progress progress, bool& cancel) { + prorgess_fn(std::move(progress), cancel); + if (cancel) { + // Upload was canceled + BOOST_LOG_TRIVIAL(info) << name << ": Upload canceled"; + res = false; + } + }) +#ifdef WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) +#endif + .perform_sync(); + + return res; +} + +void Mainsail::set_auth(Http &http) const +{ + if (!m_apikey.empty()) + http.header("X-Api-Key", m_apikey); + if (!m_cafile.empty()) + http.ca_file(m_cafile); +} + +std::string Mainsail::make_url(const std::string &path) const +{ + if (m_host.find("http://") == 0 || m_host.find("https://") == 0) { + if (m_host.back() == '/') { + return (boost::format("%1%%2%") % m_host % path).str(); + } else { + return (boost::format("%1%/%2%") % m_host % path).str(); + } + } else { + return (boost::format("http://%1%/%2%") % m_host % path).str(); + } +} + + +} diff --git a/src/slic3r/Utils/Mainsail.hpp b/src/slic3r/Utils/Mainsail.hpp new file mode 100644 index 000000000..136c7dc57 --- /dev/null +++ b/src/slic3r/Utils/Mainsail.hpp @@ -0,0 +1,64 @@ +#ifndef slic3r_Mainsail_hpp_ +#define slic3r_Mainsail_hpp_ + +#include +#include +#include +#include + +#include "PrintHost.hpp" +#include "libslic3r/PrintConfig.hpp" + + +namespace Slic3r { + +class DynamicPrintConfig; +class Http; + +// https://moonraker.readthedocs.io/en/latest/web_api +class Mainsail : public PrintHost +{ +public: + Mainsail(DynamicPrintConfig *config); + ~Mainsail() override = default; + + const char* get_name() const override; + + virtual bool test(wxString &curl_msg) const override; + wxString get_test_ok_msg () const override; + wxString get_test_failed_msg (wxString &msg) const override; + bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override; + bool has_auto_discovery() const override { return true; } + bool can_test() const override { return true; } + PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; } + std::string get_host() const override { return m_host; } + const std::string& get_apikey() const { return m_apikey; } + const std::string& get_cafile() const { return m_cafile; } + +protected: +/* +#ifdef WIN32 + virtual bool upload_inner_with_resolved_ip(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn, const boost::asio::ip::address& resolved_addr) const; +#endif + virtual bool validate_version_text(const boost::optional &version_text) const; + virtual bool upload_inner_with_host(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const; +*/ + std::string m_host; + std::string m_apikey; + std::string m_cafile; + bool m_ssl_revoke_best_effort; + + virtual void set_auth(Http &http) const; + std::string make_url(const std::string &path) const; + +private: +/* +#ifdef WIN32 + bool test_with_resolved_ip(wxString& curl_msg) const; +#endif +*/ +}; + +} + +#endif diff --git a/src/slic3r/Utils/OctoPrint.cpp b/src/slic3r/Utils/OctoPrint.cpp index d78e63185..20cea2123 100644 --- a/src/slic3r/Utils/OctoPrint.cpp +++ b/src/slic3r/Utils/OctoPrint.cpp @@ -1126,17 +1126,4 @@ wxString PrusaConnect::get_test_failed_msg(wxString& msg) const { return GUI::format_wxstr("%s: %s", _L("Could not connect to Prusa Connect"), msg); } - - - -wxString Mainsail::get_test_ok_msg() const -{ - return _(L("Connection to Mainsail/Fluidd works correctly.")); -} - -wxString Mainsail::get_test_failed_msg(wxString& msg) const -{ - return GUI::format_wxstr("%s: %s", _L("Could not connect to MainSail/Fluidd"), msg); -} - } diff --git a/src/slic3r/Utils/OctoPrint.hpp b/src/slic3r/Utils/OctoPrint.hpp index d12e28a5b..d9172f322 100644 --- a/src/slic3r/Utils/OctoPrint.hpp +++ b/src/slic3r/Utils/OctoPrint.hpp @@ -117,18 +117,6 @@ protected: void set_http_post_header_args(Http& http, PrintHostPostUploadAction post_action) const override; }; - -class Mainsail : public OctoPrint -{ -public: - Mainsail(DynamicPrintConfig* config) : OctoPrint(config) {} - ~Mainsail() override = default; - - const char* get_name() const override { return "Mainsail/Fluidd"; } - wxString get_test_ok_msg() const override; - wxString get_test_failed_msg(wxString& msg) const override; -}; - class SL1Host : public PrusaLink { public: diff --git a/src/slic3r/Utils/PrintHost.cpp b/src/slic3r/Utils/PrintHost.cpp index 5cb318715..cddada068 100644 --- a/src/slic3r/Utils/PrintHost.cpp +++ b/src/slic3r/Utils/PrintHost.cpp @@ -19,6 +19,7 @@ #include "AstroBox.hpp" #include "Repetier.hpp" #include "MKS.hpp" +#include "Mainsail.hpp" #include "../GUI/PrintHostDialogs.hpp" namespace fs = boost::filesystem; From 6fdb8c79cd5051d0095071e4646730d6ce46b8f1 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 6 Apr 2023 10:11:34 +0200 Subject: [PATCH 019/115] Mainsail: Resolve IP address from test message Windows only --- src/slic3r/Utils/Mainsail.cpp | 77 ++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 2 deletions(-) diff --git a/src/slic3r/Utils/Mainsail.cpp b/src/slic3r/Utils/Mainsail.cpp index 0953096da..84d329a22 100644 --- a/src/slic3r/Utils/Mainsail.cpp +++ b/src/slic3r/Utils/Mainsail.cpp @@ -8,17 +8,61 @@ #include #include #include +#include #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/I18N.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/format.hpp" +#include "libslic3r/AppConfig.hpp" #include "Http.hpp" namespace fs = boost::filesystem; namespace pt = boost::property_tree; namespace Slic3r { +namespace { +#ifdef WIN32 +// Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. +std::string substitute_host(const std::string& orig_addr, std::string sub_addr) +{ + // put ipv6 into [] brackets + if (sub_addr.find(':') != std::string::npos && sub_addr.at(0) != '[') + sub_addr = "[" + sub_addr + "]"; + // Using the new CURL API for handling URL. https://everything.curl.dev/libcurl/url + // If anything fails, return the input unchanged. + std::string out = orig_addr; + CURLU* hurl = curl_url(); + if (hurl) { + // Parse the input URL. + CURLUcode rc = curl_url_set(hurl, CURLUPART_URL, orig_addr.c_str(), 0); + if (rc == CURLUE_OK) { + // Replace the address. + rc = curl_url_set(hurl, CURLUPART_HOST, sub_addr.c_str(), 0); + if (rc == CURLUE_OK) { + // Extract a string fromt the CURL URL handle. + char* url; + rc = curl_url_get(hurl, CURLUPART_URL, &url, 0); + if (rc == CURLUE_OK) { + out = url; + curl_free(url); + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to extract the URL after substitution"; + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to substitute host " << sub_addr << " in URL " << orig_addr; + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to parse URL " << orig_addr; + curl_url_cleanup(hurl); + } + else + BOOST_LOG_TRIVIAL(error) << "OctoPrint substitute_host: failed to allocate curl_url"; + return out; +} +#endif +} Mainsail::Mainsail(DynamicPrintConfig *config) : m_host(config->opt_string("print_host")), m_apikey(config->opt_string("printhost_apikey")), @@ -85,6 +129,14 @@ bool Mainsail::test(wxString& msg) const msg = "Could not parse server response"; } }) +#ifdef _WIN32 + .ssl_revoke_best_effort(m_ssl_revoke_best_effort) + .on_ip_resolve([&](std::string address) { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Remember resolved address to be reused at successive REST API call. + msg = GUI::from_u8(address); + }) +#endif // _WIN32 .perform_sync(); return res; @@ -108,8 +160,29 @@ bool Mainsail::upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, Error std::string url; bool res = true; - url = make_url("server/files/upload"); - +#ifdef WIN32 + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + if (m_host.find("https://") == 0 || test_msg_or_host_ip.empty() || !GUI::get_app_config()->get_bool("allow_ip_resolve")) +#endif // _WIN32 + { + // If https is entered we assume signed ceritificate is being used + // IP resolving will not happen - it could resolve into address not being specified in cert + url = make_url("server/files/upload"); + } +#ifdef WIN32 + else { + // Workaround for Windows 10/11 mDNS resolve issue, where two mDNS resolves in succession fail. + // Curl uses easy_getinfo to get ip address of last successful transaction. + // If it got the address use it instead of the stored in "host" variable. + // This new address returns in "test_msg_or_host_ip" variable. + // Solves troubles of uploades failing with name address. + // in original address (m_host) replace host for resolved ip + info_fn(L"resolve", test_msg_or_host_ip); + url = substitute_host(make_url("server/files/upload"), GUI::into_u8(test_msg_or_host_ip)); + BOOST_LOG_TRIVIAL(info) << "Upload address after ip resolve: " << url; + } +#endif // _WIN32 + BOOST_LOG_TRIVIAL(info) << boost::format("%1%: Uploading file %2% at %3%, filename: %4%, path: %5%, print: %6%") % name % upload_data.source_path From 53d948f0d0597ea88bf9ef680f51799ec8b0e9be Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 14 Apr 2023 16:54:54 +0200 Subject: [PATCH 020/115] Organic supports: Renamed diameter_angle_scale_factor -> branch_radius_increase_per_layer Renamed diameter_scale_bp_radius -> bp_radius_increase_per_layer and removed scaling by branch diameter. --- src/libslic3r/TreeModelVolumes.cpp | 2 ++ src/libslic3r/TreeSupport.cpp | 19 ++++++++++--------- src/libslic3r/TreeSupport.hpp | 25 ++++++++++++++----------- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index e14ec5a69..5a1fcec56 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -257,6 +257,8 @@ void TreeModelVolumes::precalculate(const PrintObject& print_object, const coord auto it = radius_until_layer.find(r); if (it == radius_until_layer.end()) radius_until_layer.emplace_hint(it, r, current_layer); + else + assert(it->second >= current_layer); }; // regular radius update_radius_until_layer(ceilRadius(config.getRadius(distance_to_top, 0) + m_current_min_xy_dist_delta)); diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 240bcf7d1..4975d23f6 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -70,16 +70,17 @@ TreeSupportSettings::TreeSupportSettings(const TreeSupportMeshGroupSettings& mes maximum_move_distance_slow((angle_slow < M_PI / 2.) ? (coord_t)(tan(angle_slow) * layer_height) : std::numeric_limits::max()), support_bottom_layers(mesh_group_settings.support_bottom_enable ? (mesh_group_settings.support_bottom_height + layer_height / 2) / layer_height : 0), tip_layers(std::max((branch_radius - min_radius) / (support_line_width / 3), branch_radius / layer_height)), // Ensure lines always stack nicely even if layer height is large - diameter_angle_scale_factor(sin(mesh_group_settings.support_tree_branch_diameter_angle) * layer_height / branch_radius), + branch_radius_increase_per_layer(tan(mesh_group_settings.support_tree_branch_diameter_angle) * layer_height), max_to_model_radius_increase(mesh_group_settings.support_tree_max_diameter_increase_by_merges_when_support_to_model / 2), min_dtt_to_model(round_up_divide(mesh_group_settings.support_tree_min_height_to_model, layer_height)), increase_radius_until_radius(mesh_group_settings.support_tree_branch_diameter / 2), - increase_radius_until_layer(increase_radius_until_radius <= branch_radius ? tip_layers * (increase_radius_until_radius / branch_radius) : (increase_radius_until_radius - branch_radius) / (branch_radius * diameter_angle_scale_factor)), + increase_radius_until_layer(increase_radius_until_radius <= branch_radius ? tip_layers * (increase_radius_until_radius / branch_radius) : (increase_radius_until_radius - branch_radius) / branch_radius_increase_per_layer), support_rests_on_model(! mesh_group_settings.support_material_buildplate_only), xy_distance(mesh_group_settings.support_xy_distance), xy_min_distance(std::min(mesh_group_settings.support_xy_distance, mesh_group_settings.support_xy_distance_overhang)), bp_radius(mesh_group_settings.support_tree_bp_diameter / 2), - diameter_scale_bp_radius(std::min(sin(0.7) * layer_height / branch_radius, 1.0 / (branch_radius / (support_line_width / 2.0)))), // Either 40? or as much as possible so that 2 lines will overlap by at least 50%, whichever is smaller. + // Increase by half a line overlap, but not faster than 40 degrees angle (0 degrees means zero increase in radius). + bp_radius_increase_per_layer(std::min(tan(0.7) * layer_height, 0.5 * support_line_width)), z_distance_bottom_layers(size_t(round(double(mesh_group_settings.support_bottom_distance) / double(layer_height)))), z_distance_top_layers(size_t(round(double(mesh_group_settings.support_top_distance) / double(layer_height)))), performance_interface_skip_layers(round_up_divide(mesh_group_settings.support_interface_skip_height, layer_height)), @@ -96,7 +97,7 @@ TreeSupportSettings::TreeSupportSettings(const TreeSupportMeshGroupSettings& mes settings(mesh_group_settings), min_feature_size(mesh_group_settings.min_feature_size) { - layer_start_bp_radius = (bp_radius - branch_radius) / (branch_radius * diameter_scale_bp_radius); + layer_start_bp_radius = (bp_radius - branch_radius) / bp_radius_increase_per_layer; if (TreeSupportSettings::soluble) { // safeOffsetInc can only work in steps of the size xy_min_distance in the worst case => xy_min_distance has to be a bit larger than 0 in this worst case and should be large enough for performance to not suffer extremely @@ -1857,7 +1858,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di } radius = config.getCollisionRadius(current_elem); - const coord_t foot_radius_increase = config.branch_radius * (std::max(config.diameter_scale_bp_radius - config.diameter_angle_scale_factor, 0.0)); + const coord_t foot_radius_increase = std::max(config.bp_radius_increase_per_layer - config.branch_radius_increase_per_layer, 0.0); // Is nearly all of the time 1, but sometimes an increase of 1 could cause the radius to become bigger than recommendedMinRadius, // which could cause the radius to become bigger than precalculated. double planned_foot_increase = std::min(1.0, double(config.recommendedMinRadius(layer_idx - 1) - config.getRadius(current_elem)) / foot_radius_increase); @@ -2015,9 +2016,9 @@ static void increase_areas_one_layer( config.recommendedMinRadius(layer_idx - 1) < config.getRadius(elem.effective_radius_height + 1, elem.elephant_foot_increases)) { // can guarantee elephant foot radius increase if (ceiled_parent_radius == volumes.ceilRadius(config.getRadius(parent.state.effective_radius_height + 1, parent.state.elephant_foot_increases + 1), parent.state.use_min_xy_dist)) - extra_speed += config.branch_radius * config.diameter_scale_bp_radius; + extra_speed += config.bp_radius_increase_per_layer; else - extra_slow_speed += std::min(coord_t(config.branch_radius * config.diameter_scale_bp_radius), + extra_slow_speed += std::min(coord_t(config.bp_radius_increase_per_layer), config.maximum_move_distance - (config.maximum_move_distance_slow + extra_slow_speed)); } @@ -2236,11 +2237,11 @@ static void increase_areas_one_layer( out.to_model_gracious = first.to_model_gracious && second.to_model_gracious; // valid as we do not merge non-gracious with gracious out.elephant_foot_increases = 0; - if (config.diameter_scale_bp_radius > 0) { + if (config.bp_radius_increase_per_layer > 0) { coord_t foot_increase_radius = std::abs(std::max(config.getCollisionRadius(second), config.getCollisionRadius(first)) - config.getCollisionRadius(out)); // elephant_foot_increases has to be recalculated, as when a smaller tree with a larger elephant_foot_increases merge with a larger branch // the elephant_foot_increases may have to be lower as otherwise the radius suddenly increases. This results often in a non integer value. - out.elephant_foot_increases = foot_increase_radius / (config.branch_radius * (config.diameter_scale_bp_radius - config.diameter_angle_scale_factor)); + out.elephant_foot_increases = foot_increase_radius / (config.bp_radius_increase_per_layer - config.branch_radius_increase_per_layer); } // set last settings to the best out of both parents. If this is wrong, it will only cause a small performance penalty instead of weird behavior. diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/TreeSupport.hpp index f4ec76cda..070903096 100644 --- a/src/libslic3r/TreeSupport.hpp +++ b/src/libslic3r/TreeSupport.hpp @@ -302,9 +302,9 @@ public: */ size_t tip_layers; /*! - * \brief Factor by which to increase the branch radius. + * \brief How much a branch radius increases with each layer to guarantee the prescribed tree widening. */ - double diameter_angle_scale_factor; + double branch_radius_increase_per_layer; /*! * \brief How much a branch resting on the model may grow in radius by merging with branches that can reach the buildplate. */ @@ -330,17 +330,18 @@ public: */ coord_t xy_distance; /*! - * \brief Radius a branch should have when reaching the buildplate. + * \brief A minimum radius a tree trunk should expand to at the buildplate if possible. */ coord_t bp_radius; /*! * \brief The layer index at which an increase in radius may be required to reach the bp_radius. */ - coord_t layer_start_bp_radius; + LayerIndex layer_start_bp_radius; /*! - * \brief Factor by which to increase the branch radius to reach the required bp_radius at layer 0. Note that this radius increase will not happen in the tip, to ensure the tip is structurally sound. + * \brief How much one is allowed to increase the tree branch radius close to print bed to reach the required bp_radius at layer 0. + * Note that this radius increase will not happen in the tip, to ensure the tip is structurally sound. */ - double diameter_scale_bp_radius; + double bp_radius_increase_per_layer; /*! * \brief minimum xy_distance. Only relevant when Z overrides XY, otherwise equal to xy_distance- */ @@ -418,7 +419,9 @@ public: public: bool operator==(const TreeSupportSettings& other) const { - return branch_radius == other.branch_radius && tip_layers == other.tip_layers && diameter_angle_scale_factor == other.diameter_angle_scale_factor && layer_start_bp_radius == other.layer_start_bp_radius && bp_radius == other.bp_radius && diameter_scale_bp_radius == other.diameter_scale_bp_radius && min_radius == other.min_radius && xy_min_distance == other.xy_min_distance && // as a recalculation of the collision areas is required to set a new min_radius. + return branch_radius == other.branch_radius && tip_layers == other.tip_layers && branch_radius_increase_per_layer == other.branch_radius_increase_per_layer && layer_start_bp_radius == other.layer_start_bp_radius && bp_radius == other.bp_radius && + // as a recalculation of the collision areas is required to set a new min_radius. + bp_radius_increase_per_layer == other.bp_radius_increase_per_layer && min_radius == other.min_radius && xy_min_distance == other.xy_min_distance && xy_distance - xy_min_distance == other.xy_distance - other.xy_min_distance && // if the delta of xy_min_distance and xy_distance is different the collision areas have to be recalculated. support_rests_on_model == other.support_rests_on_model && increase_radius_until_layer == other.increase_radius_until_layer && min_dtt_to_model == other.min_dtt_to_model && max_to_model_radius_increase == other.max_to_model_radius_increase && maximum_move_distance == other.maximum_move_distance && maximum_move_distance_slow == other.maximum_move_distance_slow && z_distance_bottom_layers == other.z_distance_bottom_layers && support_line_width == other.support_line_width && support_line_spacing == other.support_line_spacing && support_roof_line_width == other.support_roof_line_width && // can not be set on a per-mesh basis currently, so code to enable processing different roof line width in the same iteration seems useless. @@ -470,9 +473,9 @@ public: { return (distance_to_top <= tip_layers ? min_radius + (branch_radius - min_radius) * distance_to_top / tip_layers : // tip branch_radius + // base - branch_radius * (distance_to_top - tip_layers) * diameter_angle_scale_factor) + (distance_to_top - tip_layers) * branch_radius_increase_per_layer) + // gradual increase - branch_radius * elephant_foot_increases * (std::max(diameter_scale_bp_radius - diameter_angle_scale_factor, 0.0)); + elephant_foot_increases * (std::max(bp_radius_increase_per_layer - branch_radius_increase_per_layer, 0.0)); } /*! @@ -502,8 +505,8 @@ public: */ [[nodiscard]] inline coord_t recommendedMinRadius(LayerIndex layer_idx) const { - double scale = (layer_start_bp_radius - int(layer_idx)) * diameter_scale_bp_radius; - return scale > 0 ? branch_radius + branch_radius * scale : 0; + double num_layers_widened = layer_start_bp_radius - layer_idx; + return num_layers_widened > 0 ? branch_radius + num_layers_widened * bp_radius_increase_per_layer : 0; } /*! From bb94e386d823facb278d14f2021e98b15e058a00 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 14 Apr 2023 17:25:07 +0200 Subject: [PATCH 021/115] =?UTF-8?q?Fixed=20SPE-1451=20Branch=20Diameter=20?= =?UTF-8?q?=3D=200=20or=201xxxx;=C2=A0result:=20slicer=20crash=20Tip=20Dia?= =?UTF-8?q?meter=20=3D=200=20or=201xxxx;;=C2=A0result:=20slicer=20crash=20?= =?UTF-8?q?fixed=20by=20limiting=20the=20diameters=20to=200.1=20..=20100mm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/libslic3r/PrintConfig.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 2d5a3b150..d48f18aa5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2908,7 +2908,8 @@ void PrintConfigDef::init_fff_params() // TRN PrintSettings: "Organic supports" > "Tip Diameter" def->tooltip = L("Branch tip diameter for organic supports."); def->sidetext = L("mm"); - def->min = 0; + def->min = 0.1f; + def->max = 100.f; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(0.8)); @@ -2919,7 +2920,8 @@ void PrintConfigDef::init_fff_params() def->tooltip = L("The diameter of the thinnest branches of organic support. Thicker branches are more sturdy. " "Branches towards the base will be thicker than this."); def->sidetext = L("mm"); - def->min = 0; + def->min = 0.1f; + def->max = 100.f; def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(2)); From 9d34998ac353bde04e7bb5bc02602f8e9441a71c Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 17 Apr 2023 10:14:56 +0200 Subject: [PATCH 022/115] ejecting via Shell COM Object --- src/slic3r/GUI/RemovableDriveManager.cpp | 108 ++++++++++++----------- src/slic3r/GUI/RemovableDriveManager.hpp | 6 -- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/slic3r/GUI/RemovableDriveManager.cpp b/src/slic3r/GUI/RemovableDriveManager.cpp index 7c8032810..133b771c8 100644 --- a/src/slic3r/GUI/RemovableDriveManager.cpp +++ b/src/slic3r/GUI/RemovableDriveManager.cpp @@ -18,6 +18,10 @@ #include #include #include + +#include +#include +#include #else // unix, linux & OSX includes #include @@ -80,7 +84,7 @@ std::vector RemovableDriveManager::search_for_removable_drives() cons namespace { - +#if 0 // From https://github.com/microsoft/Windows-driver-samples/tree/main/usb/usbview typedef struct _STRING_DESCRIPTOR_NODE { @@ -581,6 +585,57 @@ void eject_alt(std::string path, wxEvtHandler* callback_evt_handler, DriveData d if (callback_evt_handler) wxPostEvent(callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(drive_data), true))); } +#endif // 0 + +// C++ equivavalent of PowerShell script: +// $driveEject = New - Object - comObject Shell.Application +// $driveEject.Namespace(17).ParseName("E:").InvokeVerb("Eject") +// from https://superuser.com/a/1750403 +bool eject_inner(const std::string& path) +{ + std::wstring wpath = boost::nowide::widen(path); + CoInitialize(nullptr); + CComPtr pShellDisp; + HRESULT hr = pShellDisp.CoCreateInstance(CLSID_Shell, nullptr, CLSCTX_INPROC_SERVER); + if (!SUCCEEDED(hr)) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1% has failed: Attempt to get Shell pointer has failed.", path); + CoUninitialize(); + return false; + } + CComPtr pFolder; + VARIANT vtDrives; + VariantInit(&vtDrives); + vtDrives.vt = VT_I4; + vtDrives.lVal = ssfDRIVES; + hr = pShellDisp->NameSpace(vtDrives, &pFolder); + if (!SUCCEEDED(hr)) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1% has failed: Attempt to create Namespace has failed.", path); + CoUninitialize(); + return false; + } + CComPtr pItem; + hr = pFolder->ParseName(static_cast(const_cast(wpath.c_str())), &pItem); + if (!SUCCEEDED(hr)) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1% has failed: Attempt to Parse name has failed.", path); + CoUninitialize(); + return false; + } + VARIANT vtEject; + VariantInit(&vtEject); + vtEject.vt = VT_BSTR; + vtEject.bstrVal = SysAllocString(L"Eject"); + hr = pItem->InvokeVerb(vtEject); + if (!SUCCEEDED(hr)) { + BOOST_LOG_TRIVIAL(error) << GUI::format("Ejecting of %1% has failed: Attempt to Invoke Verb has failed.", path); + VariantClear(&vtEject); + CoUninitialize(); + return false; + } + BOOST_LOG_TRIVIAL(debug) << "Ejecting via InvokeVerb has succeeded."; + VariantClear(&vtEject); + CoUninitialize(); + return true; +} } // namespace // Called from UI therefore it blocks the UI thread. @@ -597,27 +652,19 @@ void RemovableDriveManager::eject_drive() BOOST_LOG_TRIVIAL(info) << "Ejecting started"; std::scoped_lock lock(m_drives_mutex); auto it_drive_data = this->find_last_save_path_drive_data(); -#if 1 if (it_drive_data != m_current_drives.end()) { - if (!eject_inner(m_last_save_path)) { + if (eject_inner(m_last_save_path)) { // success BOOST_LOG_TRIVIAL(info) << "Ejecting has succeeded."; assert(m_callback_evt_handler); if (m_callback_evt_handler) wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(*it_drive_data), true))); } else { - if (m_eject_thread.joinable()) - m_eject_thread.join(); - m_eject_thread = boost::thread(eject_alt, m_last_save_path, m_callback_evt_handler, std::move(*it_drive_data)); - // failed to eject - // this should not happen, throwing exception might be the way here - /* BOOST_LOG_TRIVIAL(error) << "Ejecting has failed."; assert(m_callback_evt_handler); if (m_callback_evt_handler) wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair(*it_drive_data, false))); - */ } } else { // drive not found in m_current_drives @@ -626,47 +673,6 @@ void RemovableDriveManager::eject_drive() if (m_callback_evt_handler) wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair({"",""}, false))); } -#endif -#if 0 - // Implementation used until 2.5.x version - // Some usb drives does not eject properly (still visible in file explorer). Some even does not write all content and eject. - if (it_drive_data != m_current_drives.end()) { - // get handle to device - std::string mpath = "\\\\.\\" + m_last_save_path; - mpath = mpath.substr(0, mpath.size() - 1); - HANDLE handle = CreateFileW(boost::nowide::widen(mpath).c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); - if (handle == INVALID_HANDLE_VALUE) { - BOOST_LOG_TRIVIAL(error) << "Ejecting " << mpath << " failed (handle == INVALID_HANDLE_VALUE): " << GetLastError(); - assert(m_callback_evt_handler); - if (m_callback_evt_handler) - wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair(*it_drive_data, false))); - return; - } - DWORD deviceControlRetVal(0); - //these 3 commands should eject device safely but they dont, the device does disappear from file explorer but the "device was safely remove" notification doesnt trigger. - //sd cards does trigger WM_DEVICECHANGE messege, usb drives dont - BOOL e1 = DeviceIoControl(handle, FSCTL_LOCK_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr); - BOOST_LOG_TRIVIAL(error) << "FSCTL_LOCK_VOLUME " << e1 << " ; " << deviceControlRetVal << " ; " << GetLastError(); - BOOL e2 = DeviceIoControl(handle, FSCTL_DISMOUNT_VOLUME, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr); - BOOST_LOG_TRIVIAL(error) << "FSCTL_DISMOUNT_VOLUME " << e2 << " ; " << deviceControlRetVal << " ; " << GetLastError(); - // some implemenatations also calls IOCTL_STORAGE_MEDIA_REMOVAL here with FALSE as third parameter, which should set PreventMediaRemoval - BOOL error = DeviceIoControl(handle, IOCTL_STORAGE_EJECT_MEDIA, nullptr, 0, nullptr, 0, &deviceControlRetVal, nullptr); - if (error == 0) { - CloseHandle(handle); - BOOST_LOG_TRIVIAL(error) << "Ejecting " << mpath << " failed (IOCTL_STORAGE_EJECT_MEDIA)" << deviceControlRetVal << " " << GetLastError(); - assert(m_callback_evt_handler); - if (m_callback_evt_handler) - wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair(*it_drive_data, false))); - return; - } - CloseHandle(handle); - BOOST_LOG_TRIVIAL(info) << "Ejecting finished"; - assert(m_callback_evt_handler); - if (m_callback_evt_handler) - wxPostEvent(m_callback_evt_handler, RemovableDriveEjectEvent(EVT_REMOVABLE_DRIVE_EJECTED, std::pair< DriveData, bool >(std::move(*it_drive_data), true))); - m_current_drives.erase(it_drive_data); - } -#endif // 0 } std::string RemovableDriveManager::get_removable_drive_path(const std::string &path) diff --git a/src/slic3r/GUI/RemovableDriveManager.hpp b/src/slic3r/GUI/RemovableDriveManager.hpp index 264066c32..4ea25ea63 100644 --- a/src/slic3r/GUI/RemovableDriveManager.hpp +++ b/src/slic3r/GUI/RemovableDriveManager.hpp @@ -106,12 +106,6 @@ private: #endif /* _WIN32 */ #endif // REMOVABLE_DRIVE_MANAGER_OS_CALLBACKS -#ifdef _WIN32 - // Another worker thread, used only to perform alt_eject method (external SD cards only). - // Does not share data with m_thread - boost::thread m_eject_thread; -#endif /* _WIN32 */ - // Called from update() to enumerate removable drives. std::vector search_for_removable_drives() const; From 1ec13fef081bd4dbf2794af8197b4c6d5083a85e Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Mon, 17 Apr 2023 10:47:08 +0200 Subject: [PATCH 023/115] Fix crash when arranging objects not fitting into the bed fixes #10278 fixes #10241 SPE-1637 --- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 8115136a5..2828fc800 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -231,8 +231,10 @@ coord_t get_skirt_offset(const Plater* plater) { // Try to subtract the skirt from the bed shape so we don't arrange outside of it. if (plater->printer_technology() == ptFFF && plater->fff_print().has_skirt()) { const auto& print = plater->fff_print(); - skirt_inset = print.config().skirts.value * print.skirt_flow().width() + - print.config().skirt_distance.value; + if (!print.objects().empty()) { + skirt_inset = print.config().skirts.value * print.skirt_flow().width() + + print.config().skirt_distance.value; + } } return scaled(skirt_inset); From ed911260eea347bd81de0bde5fa59460f9bd25cb Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Mon, 17 Apr 2023 11:05:34 +0200 Subject: [PATCH 024/115] Fix for rotation text object by draging angle input(in advance). --- src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp index 2012d138d..0d0751b45 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoEmboss.cpp @@ -359,6 +359,16 @@ bool GLGizmoEmboss::init_create(ModelVolumeType volume_type) return true; } +namespace { +TransformationType get_transformation_type(const Selection &selection) +{ + assert(selection.is_single_full_object() || selection.is_single_volume()); + return selection.is_single_volume() ? + TransformationType::Local_Relative_Joint : + TransformationType::Instance_Relative_Joint; // object +} +} // namespace + bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) { if (mouse_event.Moving()) return false; @@ -378,9 +388,8 @@ bool GLGizmoEmboss::on_mouse_for_rotation(const wxMouseEvent &mouse_event) angle -= PI / 2; // Grabber is upward // temporary rotation - const TransformationType transformation_type = m_parent.get_selection().is_single_text() ? - TransformationType::Local_Relative_Joint : TransformationType::World_Relative_Joint; - m_parent.get_selection().rotate(Vec3d(0., 0., angle), transformation_type); + Selection& selection = m_parent.get_selection(); + selection.rotate(Vec3d(0., 0., angle), get_transformation_type(selection)); angle += *m_rotate_start_angle; // move to range <-M_PI, M_PI> @@ -2880,8 +2889,7 @@ void GLGizmoEmboss::do_rotate(float relative_z_angle) Selection &selection = m_parent.get_selection(); assert(!selection.is_empty()); selection.setup_cache(); - TransformationType transformation_type = TransformationType::Local_Relative_Joint; - selection.rotate(Vec3d(0., 0., relative_z_angle), transformation_type); + selection.rotate(Vec3d(0., 0., relative_z_angle), get_transformation_type(selection)); std::string snapshot_name; // empty meand no store undo / redo // NOTE: it use L instead of _L macro because prefix _ is appended From d0f83a58c851efc817ea83fa3897b77d594554f0 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 18 Apr 2023 08:15:36 +0200 Subject: [PATCH 025/115] SPE-1661 - Fixed SLA support gizmo inactive when it is not possible to slice because of invalid data --- src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp index 2e76ebc96..0688ca5b2 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaBase.cpp @@ -59,7 +59,7 @@ void GLGizmoSlaBase::update_volumes() if (last_comp_step == slaposCount) last_comp_step = -1; - m_input_enabled = last_comp_step >= m_min_sla_print_object_step; + m_input_enabled = last_comp_step >= m_min_sla_print_object_step || po->model_object()->sla_points_status == sla::PointsStatus::UserModified; const int object_idx = m_parent.get_selection().get_object_idx(); const int instance_idx = m_parent.get_selection().get_instance_idx(); From 80bbbcf8c32487821a332b7cada0db19a902d803 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 18 Apr 2023 08:35:57 +0200 Subject: [PATCH 026/115] SPE-1354 - Render travel toolpaths using 'flat' shader --- src/slic3r/GUI/GCodeViewer.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp index 36ac85df9..bd4653ab8 100644 --- a/src/slic3r/GUI/GCodeViewer.cpp +++ b/src/slic3r/GUI/GCodeViewer.cpp @@ -697,7 +697,9 @@ void GCodeViewer::init() buffer.render_primitive_type = TBuffer::ERenderPrimitiveType::Line; buffer.vertices.format = VBuffer::EFormat::Position; #if ENABLE_GL_CORE_PROFILE - buffer.shader = OpenGLManager::get_gl_info().is_core_profile() ? "dashed_thick_lines" : "flat"; + // on MAC using the geometry shader of dashed_thick_lines is too slow + buffer.shader = "flat"; +// buffer.shader = OpenGLManager::get_gl_info().is_core_profile() ? "dashed_thick_lines" : "flat"; #else buffer.shader = "flat"; #endif // ENABLE_GL_CORE_PROFILE From 206d251f27c47d7906f1b3c8537e415551b7e914 Mon Sep 17 00:00:00 2001 From: rtyr <36745189+rtyr@users.noreply.github.com> Date: Tue, 18 Apr 2023 11:44:34 +0200 Subject: [PATCH 027/115] Sync with PrusaSlicer-settings. --- resources/profiles/PrusaResearch.idx | 9 + resources/profiles/PrusaResearch.ini | 303 +++++++++++++++++++++++---- 2 files changed, 271 insertions(+), 41 deletions(-) diff --git a/resources/profiles/PrusaResearch.idx b/resources/profiles/PrusaResearch.idx index a108d837f..3a3e2b333 100644 --- a/resources/profiles/PrusaResearch.idx +++ b/resources/profiles/PrusaResearch.idx @@ -1,4 +1,7 @@ min_slic3r_version = 2.6.0-alpha5 +1.9.0-alpha4 Updated XL and MK4 profiles. Updated PC Blend Carbon Fiber density. +1.9.0-alpha3 Updated compatibility condition for MMU1 filaments. +1.9.0-alpha2 Added profiles for Spectrum filaments. 1.9.0-alpha1 Added profiles for Original Prusa MK4. 1.9.0-alpha0 Updated output filename format. 1.7.0-alpha2 Updated compatibility condition in some filament profiles (Prusa XL). @@ -8,7 +11,13 @@ min_slic3r_version = 2.6.0-alpha1 1.6.0-alpha2 Added profile for Prusament PETG Carbon Fiber and Fiberthree F3 PA-GF30 Pro. Updated acceleration settings for Prusa MINI. 1.6.0-alpha1 Updated FW version notification. Decreased min layer time for PLA. 1.6.0-alpha0 Default top fill set to monotonic lines. Updated infill/perimeter overlap values. Updated output filename format. Enabled dynamic overhang speeds. +min_slic3r_version = 2.5.2-rc0 +1.7.3 Updated XL and MK4 profiles. Updated PC Blend Carbon Fiber density. +1.7.2 Updated compatibility condition for MMU1 filaments. +1.7.1 Added SLA materials. Updated MK4 and XL profiles. +1.7.0 Added profiles for Original Prusa MK4. min_slic3r_version = 2.5.1-rc0 +1.6.4 Fixed compatibility condition for MMU1 filaments. 1.6.3 Added SLA materials. 1.6.2 Updated compatibility condition in some filament profiles (Prusa XL). 1.6.1 Added filament profile for Prusament PETG Tungsten 75%. Updated Prusa XL profiles. diff --git a/resources/profiles/PrusaResearch.ini b/resources/profiles/PrusaResearch.ini index 019465797..d2528a70a 100644 --- a/resources/profiles/PrusaResearch.ini +++ b/resources/profiles/PrusaResearch.ini @@ -5,7 +5,7 @@ name = Prusa Research # Configuration version of this file. Config file will only be installed, if the config_version differs. # This means, the server may force the PrusaSlicer configuration to be downgraded. -config_version = 1.9.0-alpha1 +config_version = 1.9.0-alpha4 # Where to get the updates from? config_update_url = https://files.prusa3d.com/wp-content/uploads/repository/PrusaSlicer-settings-master/live/PrusaResearch/ changelog_url = https://files.prusa3d.com/?latest=slicer-profiles&lng=%1% @@ -2130,6 +2130,7 @@ default_acceleration = 1250 max_print_speed = 200 first_layer_extrusion_width = 0.5 support_material_extrusion_width = 0.37 +top_infill_extrusion_width = 0.4 gcode_resolution = 0.008 compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]==0.4 @@ -2162,6 +2163,7 @@ max_print_speed = 200 first_layer_extrusion_width = 0.5 gcode_resolution = 0.008 support_material_extrusion_width = 0.37 +top_infill_extrusion_width = 0.4 compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]==0.4 [print:0.20mm SPEED @XL 0.4] @@ -2192,6 +2194,7 @@ default_acceleration = 1250 max_print_speed = 200 first_layer_extrusion_width = 0.5 support_material_extrusion_width = 0.37 +top_infill_extrusion_width = 0.42 compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]==0.4 [print:0.30mm DRAFT @XL 0.4] @@ -3036,7 +3039,7 @@ inherits = *0.15mm*; *MK4* perimeter_speed = 45 external_perimeter_speed = 25 small_perimeter_speed = 25 -infill_speed = 90 +infill_speed = 120 solid_infill_speed = 90 top_solid_infill_speed = 40 support_material_contact_distance = 0.2 @@ -3059,6 +3062,7 @@ max_print_speed = 200 first_layer_extrusion_width = 0.5 support_material_extrusion_width = 0.37 gcode_resolution = 0.008 +top_infill_extrusion_width = 0.4 compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.4 [print:0.15mm SPEED @MK4 0.4] @@ -3089,6 +3093,7 @@ max_print_speed = 200 first_layer_extrusion_width = 0.5 support_material_extrusion_width = 0.37 gcode_resolution = 0.008 +top_infill_extrusion_width = 0.42 compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.4 [print:0.20mm QUALITY @MK4 0.4] @@ -3096,7 +3101,7 @@ inherits = *0.20mm*; *MK4* perimeter_speed = 45 external_perimeter_speed = 25 small_perimeter_speed = 25 -infill_speed = 90 +infill_speed = 120 solid_infill_speed = 90 top_solid_infill_speed = 40 support_material_contact_distance = 0.2 @@ -3119,6 +3124,7 @@ max_print_speed = 200 first_layer_extrusion_width = 0.5 gcode_resolution = 0.008 support_material_extrusion_width = 0.37 +top_infill_extrusion_width = 0.4 compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.4 [print:0.20mm SPEED @MK4 0.4] @@ -3149,6 +3155,7 @@ default_acceleration = 1000 max_print_speed = 200 first_layer_extrusion_width = 0.5 support_material_extrusion_width = 0.37 +top_infill_extrusion_width = 0.42 compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.4 [print:0.30mm DRAFT @MK4 0.4] @@ -3565,7 +3572,7 @@ compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.8 cooling = 1 compatible_printers = # For now, all but selected filaments are disabled for the MMU 2.0 -compatible_printers_condition = ! single_extruder_multi_material and printer_notes!~/.*PG.*/ +compatible_printers_condition = ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) and printer_notes!~/.*PG.*/ end_filament_gcode = "; Filament-specific end gcode" extrusion_multiplier = 1 filament_loading_speed = 28 @@ -3608,7 +3615,7 @@ min_fan_speed = 100 temperature = 210 slowdown_below_layer_time = 10 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.01{elsif nozzle_diameter[0]==0.6}0.04{else}0.05{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K18{elsif nozzle_diameter[0]==0.8};{else}M900 K30{endif} ; Filament gcode LA 1.0" -compatible_printers_condition = ! single_extruder_multi_material and printer_notes!~/.*PG.*/ +compatible_printers_condition = ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) and printer_notes!~/.*PG.*/ [filament:*PLAPG*] start_filament_gcode = "M900 K{if nozzle_diameter[0]==0.4}0.06{elsif nozzle_diameter[0]==0.25}0.14{elsif nozzle_diameter[0]==0.3}0.08{elsif nozzle_diameter[0]==0.35}0.07{elsif nozzle_diameter[0]==0.6}0.03{elsif nozzle_diameter[0]==0.5}0.035{elsif nozzle_diameter[0]==0.8}0.02{else}0{endif} ; Filament gcode\n\nM142 S36 ; set heatbreak target temp" @@ -3658,7 +3665,7 @@ start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and no temperature = 240 filament_retract_length = 1 filament_retract_lift = 0.2 -compatible_printers_condition = printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:*PET06*] inherits = *PET* @@ -3700,7 +3707,7 @@ slowdown_below_layer_time = 18 filament_retract_length = 0.8 [filament:*04PLUS*] -compatible_printers_condition = nozzle_diameter[0]>=0.4 and ! single_extruder_multi_material and printer_notes!~/.*PG.*/ +compatible_printers_condition = nozzle_diameter[0]>=0.4 and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) and printer_notes!~/.*PG.*/ [filament:*04PLUSPG*] compatible_printers_condition = nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_notes=~/.*PG.*/ @@ -3815,7 +3822,7 @@ compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.* [filament:*ABSPG*] compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 -filament_max_volumetric_speed = 14 +filament_max_volumetric_speed = 12 start_filament_gcode = "M900 K{if nozzle_diameter[0]==0.4}0.04{elsif nozzle_diameter[0]==0.25}0.1{elsif nozzle_diameter[0]==0.3}0.06{elsif nozzle_diameter[0]==0.35}0.05{elsif nozzle_diameter[0]==0.5}0.03{elsif nozzle_diameter[0]==0.6}0.02{elsif nozzle_diameter[0]==0.8}0.01{else}0{endif} ; Filament gcode\n\nM142 S40 ; set heatbreak target temp" filament_cooling_final_speed = 50 filament_cooling_initial_speed = 10 @@ -3869,7 +3876,7 @@ compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]==0.6 [filament:*PC08PG*] inherits = *PCPG* -filament_max_volumetric_speed = 20 +filament_max_volumetric_speed = 18 compatible_printers_condition = printer_model=="XL" and nozzle_diameter[0]==0.8 [filament:*PCMK4*] @@ -3885,7 +3892,7 @@ compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.6 [filament:*PC08MK4*] inherits = *PCMK4* -filament_max_volumetric_speed = 20 +filament_max_volumetric_speed = 18 compatible_printers_condition = printer_model=="MK4" and nozzle_diameter[0]==0.8 [filament:*PAPG*] @@ -4196,7 +4203,7 @@ start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and no temperature = 260 filament_retract_length = nil filament_retract_lift = 0.4 -compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:ColorFabb XT-CF20 @PG] inherits = ColorFabb XT-CF20; *PETPG*; *04PLUSPG* @@ -4317,7 +4324,7 @@ filament_colour = #804040 filament_max_volumetric_speed = 6 first_layer_temperature = 260 temperature = 260 -compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Kimya ABS Carbon @PG] inherits = Kimya ABS Carbon; *ABSPG*; *04PLUSPG* @@ -4577,7 +4584,7 @@ filament_type = PC filament_colour = #DEE0E6 filament_max_volumetric_speed = 8 filament_retract_lift = 0.2 -compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.04{else}0.07{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" [filament:Prusament PC Blend @PG] @@ -4609,12 +4616,12 @@ inherits = Prusament PC Blend first_layer_bed_temperature = 105 bed_temperature = 110 disable_fan_first_layers = 6 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes=~/.*PRINTER_MODEL_MK(2|2.5).*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes=~/.*PRINTER_MODEL_MK(2|2.5).*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusament PC Blend Carbon Fiber] inherits = Prusament PC Blend filament_cost = 90.73 -filament_density = 1.16 +filament_density = 1.22 extrusion_multiplier = 1.04 first_layer_temperature = 285 temperature = 285 @@ -4623,7 +4630,7 @@ fan_below_layer_time = 10 filament_colour = #BBBBBB filament_retract_length = nil filament_retract_lift = nil -compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusament PC Blend Carbon Fiber @PG] inherits = Prusament PC Blend Carbon Fiber; *PCPG* @@ -4667,7 +4674,7 @@ temperature = 285 first_layer_bed_temperature = 90 bed_temperature = 115 fan_below_layer_time = 10 -compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusament PA11 Carbon Fiber @PG] inherits = Prusament PA11 Carbon Fiber; *PCPG* @@ -4786,7 +4793,7 @@ inherits = *ABSC* filament_vendor = Generic filament_cost = 27.82 filament_density = 1.04 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Generic ABS @PG] inherits = Generic ABS; *ABSPG* @@ -4923,7 +4930,7 @@ renamed_from = "Generic PET" filament_vendor = Generic filament_cost = 27.82 filament_density = 1.27 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Generic PETG @PG] inherits = Generic PETG; *PETPG* @@ -5393,7 +5400,7 @@ filament_retract_lift = 0.4 filament_max_volumetric_speed = 4 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.04{else}0.08{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" filament_spool_weight = 0 -compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and printer_model!="MK2SMM" and ! single_extruder_multi_material +compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and printer_model!="MK2SMM" and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:addnorth Adura X @PG] inherits = addnorth Adura X; *PETPG* @@ -5647,7 +5654,7 @@ filament_retract_length = 1.4 filament_max_volumetric_speed = 5 filament_spool_weight = 0 filament_notes = "Please use a nozzle that is resistant to abrasive filaments, like hardened steel." -compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and printer_model!="MK2SMM" and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and printer_model!="MK2SMM" and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:addnorth Rigid X @PG] inherits = addnorth Rigid X; *PETPG*; *04PLUSPG* @@ -5693,7 +5700,7 @@ slowdown_below_layer_time = 15 min_print_speed = 20 filament_spool_weight = 0 filament_retract_length = 1 -compatible_printers_condition = printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:addnorth Textura @PG] inherits = addnorth Textura; *PLAPG* @@ -5730,7 +5737,7 @@ disable_fan_first_layers = 3 fan_below_layer_time = 60 slowdown_below_layer_time = 15 bridge_fan_speed = 20 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Filamentworld ABS @PG] inherits = Filamentworld ABS; *ABSPG* @@ -5827,7 +5834,7 @@ filament_vendor = Filament PM filament_cost = 27.82 filament_density = 1.27 filament_spool_weight = 230 -compatible_printers_condition = nozzle_diameter[0]!=0.6 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.6 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Filament PM PETG @PG] inherits = Filament PM PETG; *PETPG* @@ -5843,7 +5850,7 @@ inherits = *PLA* filament_vendor = Generic filament_cost = 25.4 filament_density = 1.24 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Generic PLA @PG] inherits = Generic PLA; *PLAPG* @@ -6092,6 +6099,220 @@ inherits = Spectrum PLA; *PLA06PG* [filament:Spectrum PLA @PG 0.8] inherits = Spectrum PLA; *PLA08PG* +[filament:Spectrum PETG Matt] +inherits = *PET* +filament_vendor = Spectrum +bed_temperature = 90 +bridge_fan_speed = 50 +extrusion_multiplier = 1.1 +disable_fan_first_layers = 1 +full_fan_speed_layer = 1 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 8 +filament_type = PETG +first_layer_bed_temperature = 85 +first_layer_temperature = 230 +max_fan_speed = 100 +min_fan_speed = 30 +temperature = 240 +filament_density = 1.35 + +[filament:Spectrum PETG Matt @PG] +inherits = Spectrum PETG Matt; *PETPG* + +[filament:Spectrum PETG Matt @PG 0.6] +inherits = Spectrum PETG Matt @PG; *PET06PG* + +[filament:Spectrum PETG Matt @PG 0.8] +inherits = Spectrum PETG Matt @PG; *PET08PG* + +[filament:Spectrum PETG Matt @MINI] +inherits = Spectrum PETG Matt; *PETMINI* + +[filament:Spectrum PETG HT100] +inherits = *PET* +filament_vendor = Spectrum +bed_temperature = 105 +bridge_fan_speed = 50 +extrusion_multiplier = 1 +disable_fan_first_layers = 1 +full_fan_speed_layer = 1 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 8 +filament_type = PETG +first_layer_bed_temperature = 105 +first_layer_temperature = 250 +max_fan_speed = 100 +min_fan_speed = 30 +temperature = 250 +filament_density = 1.24 + +[filament:Spectrum PETG HT100 @PG] +inherits = Spectrum PETG HT100; *PETPG* + +[filament:Spectrum PETG HT100 @PG 0.6] +inherits = Spectrum PETG HT100 @PG; *PET06PG* + +[filament:Spectrum PETG HT100 @PG 0.8] +inherits = Spectrum PETG HT100 @PG; *PET08PG* + +[filament:Spectrum PETG HT100 @MINI] +inherits = Spectrum PETG HT100; *PETMINI* +bed_temperature = 100 +first_layer_bed_temperature = 100 + +[filament:Spectrum GreenyHT] +inherits = *PLA* +filament_vendor = Spectrum +first_layer_temperature = 205 +first_layer_bed_temperature = 45 +temperature = 205 +bed_temperature = 45 +bridge_fan_speed = 50 +extrusion_multiplier = 1.0 +disable_fan_first_layers = 1 +full_fan_speed_layer = 1 +fan_always_on = 1 +fan_below_layer_time = 20 +filament_colour = #FF8000 +filament_max_volumetric_speed = 8 +filament_type = PLA +max_fan_speed = 100 +min_fan_speed = 30 +filament_density = 1.54 + +[filament:Spectrum GreenyHT @PG] +inherits = Spectrum GreenyHT; *PLAPG* + +[filament:Spectrum GreenyHT @PG 0.6] +inherits = Spectrum GreenyHT @PG; *PLA06PG* + +[filament:Spectrum GreenyHT @PG 0.8] +inherits = Spectrum GreenyHT @PG; *PLA08PG* + +[filament:Spectrum ASA 275] +inherits = *ABSC* +filament_vendor = Spectrum +first_layer_temperature = 237 +first_layer_bed_temperature = 80 +temperature = 237 +bed_temperature = 80 +extrusion_multiplier = 0.98 +filament_type = ASA +filament_density = 1.24 + +[filament:Spectrum ASA 275 @PG] +inherits = Spectrum ASA 275; *ABSPG* +compatible_printers_condition = nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_notes=~/.*PG.*/ and ! single_extruder_multi_material + +[filament:Spectrum ASA 275 @PG 0.6] +inherits = Spectrum ASA 275 @PG; *ABS06PG* +compatible_printers_condition = nozzle_diameter[0]==0.6 and printer_notes=~/.*PG.*/ and ! single_extruder_multi_material + +[filament:Spectrum ASA 275 @PG 0.8] +inherits = Spectrum ASA 275 @PG; *ABS08PG* +compatible_printers_condition = nozzle_diameter[0]==0.8 and printer_notes=~/.*PG.*/ and ! single_extruder_multi_material + +[filament:Spectrum ASA 275 @MINI] +inherits = Spectrum ASA 275; *ABSMINI* +temperature = 235 +bed_temperature = 80 +extrusion_multiplier = 1 + +[filament:Spectrum ASA Kevlar] +inherits = *ABSC* +filament_vendor = Spectrum +temperature = 250 +bed_temperature = 105 +extrusion_multiplier = 1.04 +filament_type = ASA +filament_density = 1.24 +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) + +[filament:Spectrum ASA Kevlar @PG] +inherits = Spectrum ASA Kevlar; *ABSPG* +compatible_printers_condition = nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_model=="XL" + +[filament:Spectrum ASA Kevlar @PG 0.6] +inherits = Spectrum ASA Kevlar @PG; *ABS06PG* + +[filament:Spectrum ASA Kevlar @PG 0.8] +inherits = Spectrum ASA Kevlar @PG; *ABS08PG* + +[filament:Spectrum ASA Kevlar @MK4] +inherits = Spectrum ASA Kevlar; *ABSMK4* +compatible_printers_condition = nozzle_diameter[0]>=0.4 and nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_model=="MK4" + +[filament:Spectrum ASA Kevlar @MK4 0.6] +inherits = Spectrum ASA Kevlar @MK4; *ABS06MK4* + +[filament:Spectrum ASA Kevlar @MK4 0.8] +inherits = Spectrum ASA Kevlar @MK4; *ABS08MK4* + +[filament:Spectrum ASA Kevlar @MINI] +inherits = Spectrum ASA Kevlar; *ABSMINI* +temperature = 250 +bed_temperature = 100 +extrusion_multiplier = 1.03 +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model=="MINI" + +[filament:Spectrum Tough PLA] +inherits = *PLA* +filament_vendor = Spectrum +temperature = 235 +bed_temperature = 45 +extrusion_multiplier = 0.95 +filament_type = PLA Tough +filament_density = 1.24 + +[filament:Spectrum Tough PLA @PG] +inherits = Spectrum Tough PLA; *PLAPG* + +[filament:Spectrum Tough PLA @PG 0.6] +inherits = Spectrum Tough PLA @PG; *PLA06PG* + +[filament:Spectrum Tough PLA @PG 0.8] +inherits = Spectrum Tough PLA @PG; *PLA08PG* + +[filament:Spectrum PLA PRO] +inherits = *PLA* +filament_vendor = Spectrum +filament_type = PLA +filament_density = 1.22 + +[filament:Spectrum PLA PRO @PG] +inherits = Spectrum PLA PRO; *PLAPG* + +[filament:Spectrum PLA PRO @PG 0.6] +inherits = Spectrum PLA PRO @PG; *PLA06PG* + +[filament:Spectrum PLA PRO @PG 0.8] +inherits = Spectrum PLA PRO @PG; *PLA08PG* + +[filament:Spectrum PCTG] +inherits = *PET* +filament_vendor = Spectrum +filament_type = PCTG +temperature = 240 +bed_temperature = 90 +filament_density = 1.27 + +[filament:Spectrum PCTG @PG] +inherits = Spectrum PCTG; *PETPG* + +[filament:Spectrum PCTG @PG 0.6] +inherits = Spectrum PCTG @PG; *PET06PG* + +[filament:Spectrum PCTG @PG 0.8] +inherits = Spectrum PCTG @PG; *PET08PG* + +[filament:Spectrum PCTG @MINI] +inherits = Spectrum PCTG; *PETMINI* + [filament:Generic FLEX] inherits = *FLEX* filament_vendor = Generic @@ -6596,7 +6817,7 @@ extrusion_multiplier = 0.95 filament_density = 1.1 first_layer_bed_temperature = 105 bed_temperature = 100 -compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]>=0.4 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Filatech FilaCarbon @PG] inherits = Filatech FilaCarbon; *ABSPG*; *04PLUSPG* @@ -6698,7 +6919,7 @@ first_layer_temperature = 230 first_layer_bed_temperature = 100 temperature = 225 bed_temperature = 110 -compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Filatech HIPS @PG] inherits = Filatech HIPS; *ABSPG* @@ -6755,7 +6976,7 @@ cooling = 0 bridge_fan_speed = 25 filament_type = PA filament_max_volumetric_speed = 8 -compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_notes!~/.*PRINTER_MODEL_MK(2|2.5).*/ and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Filatech PA @PG] inherits = Filatech PA; *ABSPG* @@ -7235,7 +7456,7 @@ min_fan_speed = 20 max_fan_speed = 20 bridge_fan_speed = 30 disable_fan_first_layers = 4 -compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) filament_notes = "Material Description\nUltrafuse® PC/ABS FR Black is a V-0 flame retardant blend of Polycarbonate and ABS – two of the most used thermoplastics for engineering & electrical applications. The combination of these two materials results in a premium material with a mix of the excellent mechanical properties of PC and the comparably low printing temperature of ABS. Combined with a halogen free flame retardant, parts printed with Ultrafuse® PC/ABS FR Black feature great tensile and impact strength, higher thermal resistance than ABS and can fulfill the requirements of the UL94 V-0 standard.\n\nPrinting Recommendations:\nApply Magigoo PC to a clean build plate to improve adhesion." start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.04{else}0.07{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" @@ -7670,7 +7891,7 @@ filament_vendor = Made for Prusa filament_cost = 27.82 filament_density = 1.08 filament_spool_weight = 230 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusa ABS @PG] inherits = Prusa ABS; *ABSPG* @@ -7881,7 +8102,7 @@ start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and no [filament:Prusament PC Blend Carbon Fiber @MMU2] inherits = Prusament PC Blend @MMU2 filament_cost = 90.73 -filament_density = 1.16 +filament_density = 1.22 extrusion_multiplier = 1.04 fan_below_layer_time = 10 first_layer_temperature = 280 @@ -7925,7 +8146,7 @@ max_fan_speed = 20 min_fan_speed = 20 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.01{elsif nozzle_diameter[0]==0.6}0.03{else}0.04{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K12{elsif nozzle_diameter[0]==0.8};{else}M900 K20{endif} ; Filament gcode LA 1.0" temperature = 220 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Generic HIPS] inherits = *ABS* @@ -7945,7 +8166,7 @@ max_fan_speed = 20 min_fan_speed = 20 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.01{elsif nozzle_diameter[0]==0.6}0.03{else}0.04{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K12{elsif nozzle_diameter[0]==0.8};{else}M900 K20{endif} ; Filament gcode LA 1.0" temperature = 230 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Generic HIPS @PG] inherits = Generic HIPS; *ABSPG* @@ -7973,7 +8194,7 @@ filament_vendor = Made for Prusa filament_cost = 27.82 filament_density = 1.27 filament_spool_weight = 230 -compatible_printers_condition = nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.6 and nozzle_diameter[0]!=0.8 and printer_model!="MK2SMM" and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusa PETG @PG] inherits = Prusa PETG; *PETPG* @@ -8239,7 +8460,7 @@ filament_vendor = Made for Prusa filament_cost = 27.82 filament_density = 1.24 filament_spool_weight = 230 -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusa PLA @PG] inherits = Prusa PLA; *PLAPG* @@ -9350,7 +9571,7 @@ filament_cost = 36.29 filament_density = 1.24 filament_spool_weight = 201 filament_notes = "Affordable filament for everyday printing in premium quality manufactured in-house by Josef Prusa" -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Prusament PLA @PG] inherits = Prusament PLA; *PLAPG* @@ -9377,7 +9598,7 @@ filament_max_volumetric_speed = 8 filament_type = PVB filament_soluble = 1 filament_colour = #FFFF6F -compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]!=0.8 and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) slowdown_below_layer_time = 20 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.05{else}0.08{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" @@ -9557,7 +9778,7 @@ temperature = 260 max_fan_speed = 0 min_fan_speed = 0 start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.6}0.12{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/ and nozzle_diameter[0]==0.8}0.06{elsif printer_notes=~/.*PRINTER_MODEL_MINI.*/}0.2{elsif nozzle_diameter[0]==0.8}0.02{elsif nozzle_diameter[0]==0.6}0.04{else}0.08{endif} ; Filament gcode LA 1.5\n{if printer_notes=~/.*PRINTER_MODEL_MINI.*/};{elsif printer_notes=~/.*PRINTER_HAS_BOWDEN.*/}M900 K200{elsif nozzle_diameter[0]==0.6}M900 K24{elsif nozzle_diameter[0]==0.8};{else}M900 K45{endif} ; Filament gcode LA 1.0" -compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:Taulman Bridge @PG] inherits = Taulman Bridge; *ABSPG* @@ -9779,7 +10000,7 @@ temperature = 285 first_layer_bed_temperature = 90 bed_temperature = 90 fan_below_layer_time = 10 -compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) max_fan_speed = 15 min_fan_speed = 15 filament_type = PA @@ -10009,7 +10230,7 @@ start_filament_gcode = "M900 K{if printer_notes=~/.*PRINTER_MODEL_MINI.*/ and no temperature = 235 filament_wipe = 0 filament_retract_lift = 0 -compatible_printers_condition = nozzle_diameter[0]>=0.35 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! single_extruder_multi_material +compatible_printers_condition = nozzle_diameter[0]>=0.35 and printer_model!="MINI" and printer_notes!~/.*PG.*/ and ! (printer_notes=~/.*PRINTER_VENDOR_PRUSA3D.*/ and printer_notes=~/.*PRINTER_MODEL_MK(2.5|3).*/ and single_extruder_multi_material) [filament:FormFutura Centaur PP @PG] inherits = FormFutura Centaur PP; *PETPG* @@ -10468,7 +10689,7 @@ compatible_printers_condition = printer_model=="MINI" and nozzle_diameter[0]!=0. [filament:Prusament PC Blend Carbon Fiber @MINI] inherits = Prusament PC Blend @MINI filament_cost = 90.73 -filament_density = 1.16 +filament_density = 1.22 extrusion_multiplier = 1.04 first_layer_temperature = 280 temperature = 280 From fdac21b8077c52aba761192a40839a298576ef54 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 18 Apr 2023 12:57:00 +0200 Subject: [PATCH 028/115] Fix of SPE-1658 GH #9665 Crash at macOS when Orgnanic Support selected Reworked (again!) connecting of islands into a Z-graph. Implemented various heuristics to handle self-intersecting and mutually intersecting ExPolygons on the same layer. --- src/libslic3r/Layer.cpp | 369 ++++++++++++++++++--------- src/libslic3r/TriangleMeshSlicer.cpp | 10 + 2 files changed, 262 insertions(+), 117 deletions(-) diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp index 7802fe983..199caf0d0 100644 --- a/src/libslic3r/Layer.cpp +++ b/src/libslic3r/Layer.cpp @@ -78,16 +78,22 @@ void Layer::make_slices() ClipperLib::ClipperOffset co; ClipperLib::Paths out2; - static constexpr const float delta = ClipperSafetyOffset; // *10.f; + // Top / bottom surfaces must overlap more than 2um to be chained into a Z graph. + // Also a larger offset will likely be more robust on non-manifold input polygons. + static constexpr const float delta = scaled(0.001); co.MiterLimit = scaled(3.); // Use the default zero edge merging distance. For this kind of safety offset the accuracy of normal direction is not important. // co.ShortestEdgeLength = delta * ClipperOffsetShortestEdgeFactor; + static constexpr const double accept_area_threshold_ccw = sqr(scaled(0.1 * delta)); + // Such a small hole should not survive the shrinkage, it should grow over + static constexpr const double accept_area_threshold_cw = sqr(scaled(0.2 * delta)); for (const ExPolygon &expoly : expolygons) { contours.clear(); co.Clear(); co.AddPath(expoly.contour.points, ClipperLib::jtMiter, ClipperLib::etClosedPolygon); co.Execute(contours, - delta); + size_t num_prev = out.size(); if (! contours.empty()) { holes.clear(); for (const Polygon &hole : expoly.holes) { @@ -109,13 +115,46 @@ void Layer::make_slices() clipper.Execute(ClipperLib::ctDifference, contours, ClipperLib::pftNonZero, ClipperLib::pftNonZero); } for (const auto &contour : contours) { - out.emplace_back(); - ClipperLib_Z::Path &path = out.back(); - path.reserve(contour.size()); - for (const Point &p : contour) - path.push_back({ p.x(), p.y(), isrc }); + bool accept = true; + // Trying to get rid of offset artifacts, that may be created due to numerical issues in offsetting algorithm + // or due to self-intersections in the source polygons. + //FIXME how reliable is it? Is it helpful or harmful? It seems to do more harm than good as it tends to punch holes + // into existing ExPolygons. +#if 0 + if (contour.size() < 8) { + // Only accept contours with area bigger than some threshold. + double a = ClipperLib::Area(contour); + // Polygon has to be bigger than some threshold to be accepted. + // Hole to be accepted has to have an area slightly bigger than the non-hole, so it will not happen due to rounding errors, + // that a hole will be accepted without its outer contour. + accept = a > 0 ? a > accept_area_threshold_ccw : a < - accept_area_threshold_cw; + } +#endif + if (accept) { + out.emplace_back(); + ClipperLib_Z::Path &path = out.back(); + path.reserve(contour.size()); + for (const Point &p : contour) + path.push_back({ p.x(), p.y(), isrc }); + } } } +#if 0 // #ifndef NDEBUG + // Test whether the expolygons in a single layer overlap. + Polygons test; + for (size_t i = num_prev; i < out.size(); ++ i) + test.emplace_back(ClipperZUtils::from_zpath(out[i])); + Polygons outside = diff(test, to_polygons(expoly)); + if (! outside.empty()) { + BoundingBox bbox(get_extents(expoly)); + bbox.merge(get_extents(test)); + SVG svg(debug_out_path("expolygons_to_zpaths_shrunk-self-intersections.svg").c_str(), bbox); + svg.draw(expoly, "blue"); + svg.draw(test, "green"); + svg.draw(outside, "red"); + } + assert(outside.empty()); +#endif // NDEBUG ++ isrc; } @@ -164,121 +203,36 @@ static void connect_layer_slices( if (polynode.Contour.size() >= 3) { // If there is an intersection point, it should indicate which contours (one from layer below, the other from layer above) intersect. // Otherwise the contour is fully inside another contour. - int32_t i = -1, j = -1; - for (int icontour = 0; icontour <= polynode.ChildCount(); ++ icontour) { - const ClipperLib_Z::Path &contour = icontour == 0 ? polynode.Contour : polynode.Childs[icontour - 1]->Contour; - if (contour.size() >= 3) { - for (const ClipperLib_Z::IntPoint &pt : contour) { - j = pt.z(); - if (j < 0) { - const auto &intersection = m_intersections[-j - 1]; - assert(intersection.first <= intersection.second); - if (intersection.second < m_offset_above) { - // Ignore intersection of polygons on the 1st layer. - assert(intersection.first >= m_offset_below); - j = i; - } else if (intersection.first >= m_offset_above) { - // Ignore intersection of polygons on the 2nd layer - assert(intersection.second < m_offset_end); - j = i; - } else { - std::tie(i, j) = m_intersections[-j - 1]; - assert(assert_intersection_valid(i, j)); - goto end; - } - } else if (i == -1) { - // First source contour of this expolygon was found. - i = j; - } else if (i != j) { - // Second source contour of this expolygon was found. - if (i > j) - std::swap(i, j); - assert(assert_intersection_valid(i, j)); - goto end; - } - } + auto [i, j] = this->find_top_bottom_contour_ids_strict(polynode); + bool found = false; + if (i < 0 && j < 0) { + // This should not happen. It may only happen if the source contours had just self intersections or intersections with contours at the same layer. + // We may safely ignore such cases where the intersection area is meager. + double a = ClipperLib_Z::Area(polynode.Contour); + if (a < sqr(scaled(0.001))) { + // Ignore tiny overlaps. They are not worth resolving. + } else { + // We should not ignore large cases. Try to resolve the conflict by a majority of references. + std::tie(i, j) = this->find_top_bottom_contour_ids_approx(polynode); + // At least top or bottom should be resolved. + assert(i >= 0 || j >= 0); } } - end: - bool found = false; - if (i == -1) { - // This should not happen. It may only happen if the source contours had just self intersections or intersections with contours at the same layer. - assert(false); - } else if (i == j) { - // The contour is completely inside another contour. - Point pt(polynode.Contour.front().x(), polynode.Contour.front().y()); - if (i < m_offset_above) { - // Index of an island below. Look-it up in the island above. - assert(i >= m_offset_below); - i -= m_offset_below; - for (int l = int(m_above.lslices_ex.size()) - 1; l >= 0; -- l) { - LayerSlice &lslice = m_above.lslices_ex[l]; - if (lslice.bbox.contains(pt) && m_above.lslices[l].contains(pt)) { - found = true; - j = l; - assert(i >= 0 && i < m_below.lslices_ex.size()); - assert(j >= 0 && j < m_above.lslices_ex.size()); - break; - } - } - //FIXME remove the following block one day, it should not be needed. - // The following shall not happen now as the source expolygons are being shrunk a bit before intersecting, - // thus each point of each intersection polygon should fit completely inside one of the original (unshrunk) expolygons. - assert(found); - if (!found) { - // The check above might sometimes fail when the polygons overlap only on points, which causes the clipper to detect no intersection. - // The problem happens rarely, mostly on simple polygons (in terms of number of points), but regardless of size! - // example of failing link on two layers, each with single polygon without holes. - // layer A = Polygon{(-24931238,-11153865),(-22504249,-8726874),(-22504249,11477151),(-23261469,12235585),(-23752371,12727276),(-25002495,12727276),(-27502745,10227026),(-27502745,-12727274),(-26504645,-12727274)} - // layer B = Polygon{(-24877897,-11100524),(-22504249,-8726874),(-22504249,11477151),(-23244827,12218916),(-23752371,12727276),(-25002495,12727276),(-27502745,10227026),(-27502745,-12727274),(-26504645,-12727274)} - // note that first point is not identical, and the check above picks (-24877897,-11100524) as the first contour point (polynode.Contour.front()). - // that point is sadly slightly outisde of the layer A, so no link is detected, eventhough they are overlaping "completely" - Polygon contour_poly(ClipperZUtils::from_zpath(polynode.Contour)); - BoundingBox contour_aabb{contour_poly.points}; - for (int l = int(m_above.lslices_ex.size()) - 1; l >= 0; --l) { - LayerSlice &lslice = m_above.lslices_ex[l]; - // it is potentially slow, but should be executed rarely - if (contour_aabb.overlap(lslice.bbox) && !intersection(Polygons{contour_poly}, m_above.lslices[l]).empty()) { - found = true; - j = l; - assert(i >= 0 && i < m_below.lslices_ex.size()); - assert(j >= 0 && j < m_above.lslices_ex.size()); - break; - } - } - } + if (j < 0) { + if (i < 0) { + // this->find_top_bottom_contour_ids_approx() shoudl have made sure this does not happen. + assert(false); } else { - // Index of an island above. Look-it up in the island below. - assert(j < m_offset_end); - j -= m_offset_above; - for (int l = int(m_below.lslices_ex.size()) - 1; l >= 0; -- l) { - LayerSlice &lslice = m_below.lslices_ex[l]; - if (lslice.bbox.contains(pt) && m_below.lslices[l].contains(pt)) { - found = true; - i = l; - assert(i >= 0 && i < m_below.lslices_ex.size()); - assert(j >= 0 && j < m_above.lslices_ex.size()); - break; - } - } - //FIXME remove the following block one day, it should not be needed. - // The following shall not happen now as the source expolygons are being shrunk a bit before intersecting, - // thus each point of each intersection polygon should fit completely inside one of the original (unshrunk) expolygons. - if (!found) { // Explanation for aditional check is above. - Polygon contour_poly(ClipperZUtils::from_zpath(polynode.Contour)); - BoundingBox contour_aabb{contour_poly.points}; - for (int l = int(m_below.lslices_ex.size()) - 1; l >= 0; --l) { - LayerSlice &lslice = m_below.lslices_ex[l]; - if (contour_aabb.overlap(lslice.bbox) && !intersection(Polygons{contour_poly}, m_below.lslices[l]).empty()) { - found = true; - i = l; - assert(i >= 0 && i < m_below.lslices_ex.size()); - assert(j >= 0 && j < m_above.lslices_ex.size()); - break; - } - } - } + assert(i >= m_offset_below && i < m_offset_above); + i -= m_offset_below; + j = this->find_other_contour_costly(polynode, m_above, j == -2); + found = j >= 0; } + } else if (i < 0) { + assert(j >= m_offset_above && j < m_offset_end); + j -= m_offset_above; + i = this->find_other_contour_costly(polynode, m_below, i == -2); + found = i >= 0; } else { assert(assert_intersection_valid(i, j)); i -= m_offset_below; @@ -329,6 +283,187 @@ static void connect_layer_slices( } private: + // Find the indices of the contour below & above for an expolygon created as an intersection of two expolygons, one below, the other above. + // Returns -1 if there is no point on the intersection refering bottom resp. top source expolygon. + // Returns -2 if the intersection refers to multiple source expolygons on bottom resp. top layers. + std::pair find_top_bottom_contour_ids_strict(const ClipperLib_Z::PolyNode &polynode) const + { + // If there is an intersection point, it should indicate which contours (one from layer below, the other from layer above) intersect. + // Otherwise the contour is fully inside another contour. + int32_t i = -1, j = -1; + auto process_i = [&i, &j](coord_t k) { + if (i == -1) + i = k; + else if (i >= 0) { + if (i != k) { + // Error: Intersection contour contains points of two or more source bottom contours. + i = -2; + if (j == -2) + // break + return true; + } + } else + assert(i == -2); + return false; + }; + auto process_j = [&i, &j](coord_t k) { + if (j == -1) + j = k; + else if (j >= 0) { + if (j != k) { + // Error: Intersection contour contains points of two or more source top contours. + j = -2; + if (i == -2) + // break + return true; + } + } else + assert(j == -2); + return false; + }; + for (int icontour = 0; icontour <= polynode.ChildCount(); ++ icontour) { + const ClipperLib_Z::Path &contour = icontour == 0 ? polynode.Contour : polynode.Childs[icontour - 1]->Contour; + if (contour.size() >= 3) { + for (const ClipperLib_Z::IntPoint &pt : contour) + if (coord_t k = pt.z(); k < 0) { + const auto &intersection = m_intersections[-k - 1]; + assert(intersection.first <= intersection.second); + if (intersection.first < m_offset_above ? process_i(intersection.first) : process_j(intersection.first)) + goto end; + if (intersection.second < m_offset_above ? process_i(intersection.second) : process_j(intersection.second)) + goto end; + } else if (k < m_offset_above ? process_i(k) : process_j(k)) + goto end; + } + } + end: + return { i, j }; + } + + // Find the indices of the contour below & above for an expolygon created as an intersection of two expolygons, one below, the other above. + // This variant expects that the source expolygon assingment is not unique, it counts the majority. + // Returns -1 if there is no point on the intersection refering bottom resp. top source expolygon. + // Returns -2 if the intersection refers to multiple source expolygons on bottom resp. top layers. + std::pair find_top_bottom_contour_ids_approx(const ClipperLib_Z::PolyNode &polynode) const + { + // 1) Collect histogram of contour references. + struct HistoEl { + int32_t id; + int32_t count; + }; + std::vector histogram; + { + auto increment_counter = [&histogram](const int32_t i) { + auto it = std::lower_bound(histogram.begin(), histogram.end(), i, [](auto l, auto r){ return l.id < r; }); + if (it == histogram.end() || it->id != i) + histogram.insert(it, HistoEl{ i, int32_t(1) }); + else + ++ it->count; + }; + for (int icontour = 0; icontour <= polynode.ChildCount(); ++ icontour) { + const ClipperLib_Z::Path &contour = icontour == 0 ? polynode.Contour : polynode.Childs[icontour - 1]->Contour; + if (contour.size() >= 3) { + for (const ClipperLib_Z::IntPoint &pt : contour) + if (coord_t k = pt.z(); k < 0) { + const auto &intersection = m_intersections[-k - 1]; + assert(intersection.first <= intersection.second); + increment_counter(intersection.first); + increment_counter(intersection.second); + } else + increment_counter(k); + } + } + assert(! histogram.empty()); + } + int32_t i = -1; + int32_t j = -1; + if (! histogram.empty()) { + // 2) Split the histogram to bottom / top. + auto mid = std::upper_bound(histogram.begin(), histogram.end(), m_offset_above, [](auto l, auto r){ return l < r.id; }); + // 3) Sort the bottom / top parts separately. + auto bottom_begin = histogram.begin(); + auto bottom_end = mid; + auto top_begin = mid; + auto top_end = histogram.end(); + std::sort(bottom_begin, bottom_end, [](auto l, auto r) { return l.count > r.count; }); + std::sort(top_begin, top_end, [](auto l, auto r) { return l.count > r.count; }); + double i_quality = 0; + double j_quality = 0; + if (bottom_begin != bottom_end) { + i = bottom_begin->id; + i_quality = std::next(bottom_begin) == bottom_end ? std::numeric_limits::max() : double(bottom_begin->count) / std::next(bottom_begin)->count; + } + if (top_begin != top_end) { + j = top_begin->id; + j_quality = std::next(top_begin) == top_end ? std::numeric_limits::max() : double(top_begin->count) / std::next(top_begin)->count; + } + // Expected to be called only if there are duplicate references to be resolved by the histogram. + assert(i >= 0 || j >= 0); + assert(i_quality < std::numeric_limits::max() || j_quality < std::numeric_limits::max()); + if (i >= 0 && i_quality < j_quality) { + // Force the caller to resolve the bottom references the costly but robust way. + assert(j >= 0); + // Twice the number of references for the best contour. + assert(j_quality >= 2.); + i = -2; + } else if (j >= 0) { + // Force the caller to resolve the top reference the costly but robust way. + assert(i >= 0); + // Twice the number of references for the best contour. + assert(i_quality >= 2.); + j = -2; + } + + } + return { i, j }; + } + + static int32_t find_other_contour_costly(const ClipperLib_Z::PolyNode &polynode, const Layer &other_layer, bool other_has_duplicates) + { + if (! other_has_duplicates) { + // The contour below is likely completely inside another contour above. Look-it up in the island above. + Point pt(polynode.Contour.front().x(), polynode.Contour.front().y()); + for (int i = int(other_layer.lslices_ex.size()) - 1; i >= 0; -- i) + if (other_layer.lslices_ex[i].bbox.contains(pt) && other_layer.lslices[i].contains(pt)) + return i; + // The following shall not happen now as the source expolygons are being shrunk a bit before intersecting, + // thus each point of each intersection polygon should fit completely inside one of the original (unshrunk) expolygons. + assert(false); + } + // The comment below may not be valid anymore, see the comment above. However the code is used in case the polynode contains multiple references + // to other_layer expolygons, thus the references are not unique. + // + // The check above might sometimes fail when the polygons overlap only on points, which causes the clipper to detect no intersection. + // The problem happens rarely, mostly on simple polygons (in terms of number of points), but regardless of size! + // example of failing link on two layers, each with single polygon without holes. + // layer A = Polygon{(-24931238,-11153865),(-22504249,-8726874),(-22504249,11477151),(-23261469,12235585),(-23752371,12727276),(-25002495,12727276),(-27502745,10227026),(-27502745,-12727274),(-26504645,-12727274)} + // layer B = Polygon{(-24877897,-11100524),(-22504249,-8726874),(-22504249,11477151),(-23244827,12218916),(-23752371,12727276),(-25002495,12727276),(-27502745,10227026),(-27502745,-12727274),(-26504645,-12727274)} + // note that first point is not identical, and the check above picks (-24877897,-11100524) as the first contour point (polynode.Contour.front()). + // that point is sadly slightly outisde of the layer A, so no link is detected, eventhough they are overlaping "completely" + Polygons contour_poly{ Polygon{ClipperZUtils::from_zpath(polynode.Contour)} }; + BoundingBox contour_aabb{contour_poly.front().points}; + int32_t i_largest = -1; + double a_largest = 0; + for (int i = int(other_layer.lslices_ex.size()) - 1; i >= 0; -- i) + if (contour_aabb.overlap(other_layer.lslices_ex[i].bbox)) + // it is potentially slow, but should be executed rarely + if (Polygons overlap = intersection(contour_poly, other_layer.lslices[i]); ! overlap.empty()) + if (other_has_duplicates) { + // Find the contour with the largest overlap. It is expected that the other overlap will be very small. + double a = area(overlap); + if (a > a_largest) { + a_largest = a; + i_largest = i; + } + } else { + // Most likely there is just one contour that overlaps, however it is not guaranteed. + i_largest = i; + break; + } + assert(i_largest >= 0); + return i_largest; + } + const std::vector> &m_intersections; Layer &m_below; Layer &m_above; diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 329c1c4ca..460cd901e 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -1911,6 +1911,16 @@ std::vector slice_mesh_ex( this_mode == MeshSlicingParams::SlicingMode::EvenOdd ? ClipperLib::pftEvenOdd : this_mode == MeshSlicingParams::SlicingMode::PositiveLargestContour ? ClipperLib::pftPositive : ClipperLib::pftNonZero, &expolygons); + +#if 0 +//#ifndef _NDEBUG + // Test whether the expolygons in a single layer overlap. + for (size_t i = 0; i < expolygons.size(); ++ i) + for (size_t j = i + 1; j < expolygons.size(); ++ j) { + Polygons overlap = intersection(expolygons[i], expolygons[j]); + assert(overlap.empty()); + } +#endif #if 0 //#ifndef _NDEBUG for (const ExPolygon &ex : expolygons) { From 4a05973ea8f0e1def804b1b138b26c3b2a641d22 Mon Sep 17 00:00:00 2001 From: Filip Sykala - NTB T15p Date: Tue, 18 Apr 2023 13:06:58 +0200 Subject: [PATCH 029/115] Nicering of code --- src/libslic3r/Emboss.cpp | 19 ++++++++----------- src/slic3r/GUI/Jobs/EmbossJob.cpp | 13 +++++-------- src/slic3r/Utils/EmbossStyleManager.cpp | 3 +-- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/Emboss.cpp b/src/libslic3r/Emboss.cpp index e7055e810..81a9154e1 100644 --- a/src/libslic3r/Emboss.cpp +++ b/src/libslic3r/Emboss.cpp @@ -796,8 +796,7 @@ const Glyph* priv::get_glyph( auto glyph_item = cache.find(unicode); if (glyph_item != cache.end()) return &glyph_item->second; - unsigned int font_index = font_prop.collection_number.has_value()? - *font_prop.collection_number : 0; + unsigned int font_index = font_prop.collection_number.value_or(0); if (!is_valid(font, font_index)) return nullptr; if (!font_info_opt.has_value()) { @@ -835,11 +834,10 @@ const Glyph* priv::get_glyph( glyph_opt->shape = Slic3r::union_ex(offset_ex(glyph_opt->shape, delta)); } if (font_prop.skew.has_value()) { - const float &ratio = *font_prop.skew; - auto skew = [&ratio](Polygon &polygon) { - for (Slic3r::Point &p : polygon.points) { - p.x() += p.y() * ratio; - } + double ratio = *font_prop.skew; + auto skew = [&ratio](Polygon &polygon) { + for (Slic3r::Point &p : polygon.points) + p.x() += static_cast(std::round(p.y() * ratio)); }; for (ExPolygon &expolygon : glyph_opt->shape) { skew(expolygon.contour); @@ -1363,10 +1361,9 @@ std::string Emboss::create_range_text(const std::string &text, double Emboss::get_shape_scale(const FontProp &fp, const FontFile &ff) { - const auto &cn = fp.collection_number; - unsigned int font_index = (cn.has_value()) ? *cn : 0; - int unit_per_em = ff.infos[font_index].unit_per_em; - double scale = fp.size_in_mm / unit_per_em; + size_t font_index = fp.collection_number.value_or(0); + const FontFile::Info &info = ff.infos[font_index]; + double scale = fp.size_in_mm / (double) info.unit_per_em; // Shape is scaled for store point coordinate as integer return scale * SHAPE_SCALE; } diff --git a/src/slic3r/GUI/Jobs/EmbossJob.cpp b/src/slic3r/GUI/Jobs/EmbossJob.cpp index aa2c7590e..6d41907c2 100644 --- a/src/slic3r/GUI/Jobs/EmbossJob.cpp +++ b/src/slic3r/GUI/Jobs/EmbossJob.cpp @@ -430,16 +430,13 @@ TriangleMesh priv::try_create_mesh(DataBase &input, Fnc was_canceled) { ExPolygons shapes = priv::create_shape(input, was_canceled); if (shapes.empty()) return {}; - if (was_canceled()) return {}; + if (was_canceled()) return {}; const FontProp &prop = input.text_configuration.style.prop; - const std::optional &cn = prop.collection_number; - unsigned int font_index = (cn.has_value()) ? *cn : 0; - const FontFileWithCache &font = input.font_file; - assert(font_index < font.font_file->infos.size()); - int unit_per_em = font.font_file->infos[font_index].unit_per_em; - float scale = prop.size_in_mm / unit_per_em; - float depth = prop.emboss / scale; + const FontFile &ff = *input.font_file.font_file; + // NOTE: SHAPE_SCALE is applied in ProjectZ + double scale = get_shape_scale(prop, ff) / SHAPE_SCALE; + double depth = prop.emboss / scale; auto projectZ = std::make_unique(depth); ProjectScale project(std::move(projectZ), scale); if (was_canceled()) return {}; diff --git a/src/slic3r/Utils/EmbossStyleManager.cpp b/src/slic3r/Utils/EmbossStyleManager.cpp index 100a532b8..4f066b9c8 100644 --- a/src/slic3r/Utils/EmbossStyleManager.cpp +++ b/src/slic3r/Utils/EmbossStyleManager.cpp @@ -472,8 +472,7 @@ ImFont *StyleManager::create_imgui_font(const std::string &text, double scale) // TODO: start using merge mode //font_config.MergeMode = true; - const auto &cn = font_prop.collection_number; - unsigned int font_index = (cn.has_value()) ? *cn : 0; + unsigned int font_index = font_prop.collection_number.value_or(0); const auto &font_info = font_file.infos[font_index]; if (font_prop.char_gap.has_value()) { float coef = font_size / (double) font_info.unit_per_em; From bdedea307258cb3ce063bde9e7461dd28ce8811a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 19 Apr 2023 09:57:03 +0200 Subject: [PATCH 030/115] Organic supports: Speed up by slicing branches and merging polygons at the same time, thus reducing memory consumption. --- src/libslic3r/TreeSupport.cpp | 319 +++++++++++++++++++++------------- 1 file changed, 194 insertions(+), 125 deletions(-) diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 4975d23f6..174e91a9c 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -1464,17 +1464,21 @@ static void generate_initial_areas( const size_t num_support_roof_layers = mesh_group_settings.support_roof_enable ? (mesh_group_settings.support_roof_height + config.layer_height / 2) / config.layer_height : 0; const bool roof_enabled = num_support_roof_layers > 0; const bool force_tip_to_roof = sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area && roof_enabled; - //FIXME mesh_group_settings.support_angle does not apply to enforcers and also it does not apply to automatic support angle (by half the external perimeter width). - //used by max_overhang_insert_lag, only if not min_xy_dist. - const coord_t max_overhang_speed = mesh_group_settings.support_angle < 0.5 * M_PI ? coord_t(tan(mesh_group_settings.support_angle) * config.layer_height) : std::numeric_limits::max(); // cap for how much layer below the overhang a new support point may be added, as other than with regular support every new inserted point // may cause extra material and time cost. Could also be an user setting or differently calculated. Idea is that if an overhang // does not turn valid in double the amount of layers a slope of support angle would take to travel xy_distance, nothing reasonable will come from it. // The 2*z_distance_delta is only a catch for when the support angle is very high. // Used only if not min_xy_dist. - const coord_t max_overhang_insert_lag = config.z_distance_top_layers > 0 ? - std::max(round_up_divide(config.xy_distance, max_overhang_speed / 2), 2 * config.z_distance_top_layers) : - 0; + coord_t max_overhang_insert_lag = 0; + if (config.z_distance_top_layers > 0) { + max_overhang_insert_lag = 2 * config.z_distance_top_layers; + if (mesh_group_settings.support_angle > EPSILON && mesh_group_settings.support_angle < 0.5 * M_PI - EPSILON) { + //FIXME mesh_group_settings.support_angle does not apply to enforcers and also it does not apply to automatic support angle (by half the external perimeter width). + //used by max_overhang_insert_lag, only if not min_xy_dist. + const auto max_overhang_speed = coord_t(tan(mesh_group_settings.support_angle) * config.layer_height); + max_overhang_insert_lag = std::max(max_overhang_insert_lag, round_up_divide(config.xy_distance, max_overhang_speed / 2)); + } + } size_t num_support_layers; int raft_contact_layer_idx; @@ -3709,12 +3713,13 @@ static std::pair discretize_circle(const Vec3f ¢er, const Vec3f &n return { begin, int(pts.size()) }; } -static void extrude_branch( - const std::vector &path, - const TreeSupportSettings &config, - const SlicingParameters &slicing_params, - const std::vector &move_bounds, - indexed_triangle_set &result) +// Returns Z span of the generated mesh. +static std::pair extrude_branch( + const std::vector &path, + const TreeSupportSettings &config, + const SlicingParameters &slicing_params, + const std::vector &move_bounds, + indexed_triangle_set &result) { Vec3d p1, p2, p3; Vec3d v1, v2; @@ -3727,6 +3732,8 @@ static void extrude_branch( // char fname[2048]; // static int irun = 0; + float zmin, zmax; + for (size_t ipath = 1; ipath < path.size(); ++ ipath) { const SupportElement &prev = *path[ipath - 1]; const SupportElement ¤t = *path[ipath]; @@ -3743,6 +3750,7 @@ static void extrude_branch( angle_step = M_PI / (2. * nsteps); int ifan = int(result.vertices.size()); result.vertices.emplace_back((p1 - nprev * radius).cast()); + zmin = result.vertices.back().z(); float angle = angle_step; for (int i = 1; i < nsteps; ++ i, angle += angle_step) { std::pair strip = discretize_circle((p1 - nprev * radius * cos(angle)).cast(), nprev.cast(), radius * sin(angle), eps, result.vertices); @@ -3773,6 +3781,7 @@ static void extrude_branch( } int ifan = int(result.vertices.size()); result.vertices.emplace_back((p2 + ncurrent * radius).cast()); + zmax = result.vertices.back().z(); triangulate_fan(result, ifan, prev_strip.first, prev_strip.second); // sprintf(fname, "d:\\temp\\meshes\\tree-partial-%d.obj", ++ irun); // its_write_obj(result, fname); @@ -3800,6 +3809,8 @@ static void extrude_branch( } #endif } + + return std::make_pair(zmin, zmax); } #endif @@ -4121,15 +4132,13 @@ static void organic_smooth_branches_avoid_collisions( #endif // TREE_SUPPORT_ORGANIC_NUDGE_NEW // Organic specific: Smooth branches and produce one cummulative mesh to be sliced. -static indexed_triangle_set draw_branches( +static std::vector draw_branches( PrintObject &print_object, - const TreeModelVolumes &volumes, + TreeModelVolumes &volumes, const TreeSupportSettings &config, std::vector &move_bounds, std::function throw_on_cancel) { - static int irun = 0; - // All SupportElements are put into a layer independent storage to improve parallelization. std::vector> elements_with_link_down; std::vector linear_data_layers; @@ -4176,127 +4185,188 @@ static indexed_triangle_set draw_branches( organic_smooth_branches_avoid_collisions(print_object, volumes, config, move_bounds, elements_with_link_down, linear_data_layers, throw_on_cancel); + // Reduce memory footprint. After this point only finalize_interface_and_support_areas() will use volumes and from that only collisions with zero radius will be used. + volumes.clear_all_but_object_collision(); + // Unmark all nodes. for (SupportElements &elements : move_bounds) for (SupportElement &element : elements) element.state.marked = false; // Traverse all nodes, generate tubes. - // Traversal stack with nodes and thier current parent - const SlicingParameters &slicing_params = print_object.slicing_parameters(); - std::vector path; - indexed_triangle_set cummulative_mesh; - indexed_triangle_set partial_mesh; - indexed_triangle_set temp_mesh; - for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) { - SupportElements &layer = move_bounds[layer_idx]; - SupportElements &layer_above = move_bounds[layer_idx + 1]; + // Traversal stack with nodes and their current parent - for (SupportElement &start_element : layer) - if (! start_element.state.marked && ! start_element.parents.empty()) { - // Collect elements up to a bifurcation above. - start_element.state.marked = true; - for (size_t parent_idx = 0; parent_idx < start_element.parents.size(); ++ parent_idx) { - path.clear(); - path.emplace_back(&start_element); - // Traverse each branch until it branches again. - SupportElement &first_parent = layer_above[start_element.parents[parent_idx]]; - assert(path.back()->state.layer_idx + 1 == first_parent.state.layer_idx); - path.emplace_back(&first_parent); - if (first_parent.parents.size() < 2) - first_parent.state.marked = true; - if (first_parent.parents.size() == 1) { - for (SupportElement *parent = &first_parent;;) { - SupportElement &next_parent = move_bounds[parent->state.layer_idx + 1][parent->parents.front()]; - assert(path.back()->state.layer_idx + 1 == next_parent.state.layer_idx); - path.emplace_back(&next_parent); - if (next_parent.parents.size() > 1) - break; - next_parent.state.marked = true; - if (next_parent.parents.size() == 0) - break; - parent = &next_parent; + struct Branch { + std::vector path; + bool has_root{ false }; + bool has_tip { false }; + }; + + struct Slice { + Polygons polygons; + size_t num_branches{ 0 }; + }; + + struct Tree { + std::vector branches; + + std::vector slices; + LayerIndex first_layer_id{ -1 }; + }; + + std::vector trees; + + struct TreeVisitor { + static void visit_recursive(std::vector &move_bounds, SupportElement &start_element, Tree &out) { + assert(! start_element.state.marked && ! start_element.parents.empty()); + // Collect elements up to a bifurcation above. + start_element.state.marked = true; + // For each branch bifurcating from this point: + SupportElements &layer = move_bounds[start_element.state.layer_idx]; + SupportElements &layer_above = move_bounds[start_element.state.layer_idx + 1]; + bool root = out.branches.empty(); + for (size_t parent_idx = 0; parent_idx < start_element.parents.size(); ++ parent_idx) { + Branch branch; + branch.path.emplace_back(&start_element); + // Traverse each branch until it branches again. + SupportElement &first_parent = layer_above[start_element.parents[parent_idx]]; + assert(branch.path.back()->state.layer_idx + 1 == first_parent.state.layer_idx); + branch.path.emplace_back(&first_parent); + if (first_parent.parents.size() < 2) + first_parent.state.marked = true; + SupportElement *next_branch = nullptr; + if (first_parent.parents.size() == 1) + for (SupportElement *parent = &first_parent;;) { + SupportElement &next_parent = move_bounds[parent->state.layer_idx + 1][parent->parents.front()]; + assert(branch.path.back()->state.layer_idx + 1 == next_parent.state.layer_idx); + branch.path.emplace_back(&next_parent); + if (next_parent.parents.size() > 1) { + next_branch = &next_parent; + break; } + next_parent.state.marked = true; + if (next_parent.parents.size() == 0) + break; + parent = &next_parent; } + assert(branch.path.size() >= 2); + branch.has_root = root; + branch.has_tip = ! next_branch; + out.branches.emplace_back(std::move(branch)); + if (next_branch) + visit_recursive(move_bounds, *next_branch, out); + } + } + }; + + for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) + for (SupportElement &start_element : move_bounds[layer_idx]) + if (! start_element.state.marked && ! start_element.parents.empty()) { + trees.push_back({}); + TreeVisitor::visit_recursive(move_bounds, start_element, trees.back()); + assert(! trees.back().branches.empty()); + } + + const SlicingParameters &slicing_params = print_object.slicing_parameters(); + MeshSlicingParams mesh_slicing_params; + mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; + tbb::parallel_for(tbb::blocked_range(0, trees.size()), + [&trees, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { + indexed_triangle_set partial_mesh; + std::vector slice_z; + for (size_t tree_id = range.begin(); tree_id < range.end(); ++ tree_id) { + Tree &tree = trees[tree_id]; + for (const Branch &branch : tree.branches) { // Triangulate the tube. partial_mesh.clear(); - extrude_branch(path, config, slicing_params, move_bounds, partial_mesh); -#if 0 - { - char fname[2048]; - static int irun = 0; - sprintf(fname, "d:\\temp\\meshes\\tree-raw-%d.obj", ++ irun); - its_write_obj(partial_mesh, fname); - #if 0 - temp_mesh.clear(); - cut_mesh(partial_mesh, layer_z(slicing_params, path.back()->state.layer_idx) + EPSILON, nullptr, &temp_mesh, false); - sprintf(fname, "d:\\temp\\meshes\\tree-trimmed1-%d.obj", irun); - its_write_obj(temp_mesh, fname); - partial_mesh.clear(); - cut_mesh(temp_mesh, layer_z(slicing_params, path.front()->state.layer_idx) - EPSILON, &partial_mesh, nullptr, false); - sprintf(fname, "d:\\temp\\meshes\\tree-trimmed2-%d.obj", irun); - #endif - its_write_obj(partial_mesh, fname); + std::pair zspan = extrude_branch(branch.path, config, slicing_params, move_bounds, partial_mesh); + LayerIndex layer_begin = branch.has_root ? + branch.path.front()->state.layer_idx : + std::min(branch.path.front()->state.layer_idx, layer_idx_ceil(slicing_params, config, zspan.first)); + LayerIndex layer_end = (branch.has_tip ? + branch.path.back()->state.layer_idx : + std::max(branch.path.back()->state.layer_idx, layer_idx_floor(slicing_params, config, zspan.second))) + 1; + slice_z.clear(); + for (LayerIndex layer_idx = layer_begin; layer_idx < layer_end; ++ layer_idx) { + const double print_z = layer_z(slicing_params, config, layer_idx); + const double bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.; + slice_z.emplace_back(float(0.5 * (bottom_z + print_z))); } -#endif - its_merge(cummulative_mesh, partial_mesh); + std::vector slices = slice_mesh(partial_mesh, slice_z, mesh_slicing_params, throw_on_cancel); + size_t num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); + layer_begin += LayerIndex(num_empty); + for (; slices.back().empty(); -- layer_end); + LayerIndex new_begin = tree.first_layer_id == -1 ? layer_begin : std::min(tree.first_layer_id, layer_begin); + LayerIndex new_end = tree.first_layer_id == -1 ? layer_end : std::max(tree.first_layer_id + LayerIndex(tree.slices.size()), layer_end); + size_t new_size = size_t(new_end - new_begin); + if (tree.first_layer_id == -1) { + } else if (tree.slices.capacity() < new_size) { + std::vector new_slices; + new_slices.reserve(new_size); + if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) + new_slices.insert(new_slices.end(), dif, {}); + append(new_slices, std::move(tree.slices)); + tree.slices.swap(new_slices); + } else if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) + tree.slices.insert(tree.slices.begin(), tree.first_layer_id - new_begin, {}); + tree.slices.insert(tree.slices.end(), new_size - tree.slices.size(), {}); + layer_begin -= LayerIndex(num_empty); + for (LayerIndex i = layer_begin; i != layer_end; ++ i) + if (Polygons &src = slices[i - layer_begin]; ! src.empty()) { + Slice &dst = tree.slices[i - new_begin]; + if (++ dst.num_branches > 1) + append(dst.polygons, std::move(src)); + else + dst.polygons = std::move(std::move(src)); + } + tree.first_layer_id = new_begin; } - throw_on_cancel(); } - } - return cummulative_mesh; -} - -// Organic specific: Slice the cummulative mesh produced by draw_branches(). -static void slice_branches( - PrintObject &print_object, - const TreeModelVolumes &volumes, - const TreeSupportSettings &config, - const std::vector &overhangs, - std::vector &move_bounds, - const indexed_triangle_set &cummulative_mesh, - - SupportGeneratorLayersPtr &bottom_contacts, - SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayersPtr &intermediate_layers, - SupportGeneratorLayerStorage &layer_storage, - - std::function throw_on_cancel) -{ - const SlicingParameters &slicing_params = print_object.slicing_parameters(); - std::vector slice_z; - for (size_t layer_idx = 0; layer_idx < move_bounds.size(); ++ layer_idx) { - const double print_z = layer_z(print_object.slicing_parameters(), config, layer_idx); - const double bottom_z = layer_idx > 0 ? layer_z(print_object.slicing_parameters(), config, layer_idx - 1) : 0.; - slice_z.emplace_back(float(0.5 * (bottom_z + print_z))); - } - // Remove the trailing slices. - while (! slice_z.empty()) - if (move_bounds[slice_z.size() - 1].empty()) - slice_z.pop_back(); - else - break; - -#if 0 - its_write_obj(cummulative_mesh, "d:\\temp\\meshes\\tree.obj"); -#endif - - MeshSlicingParamsEx params; - params.closing_radius = float(print_object.config().slice_closing_radius.value); - params.mode = MeshSlicingParams::SlicingMode::Positive; - std::vector slices = slice_mesh_ex(cummulative_mesh, slice_z, params, throw_on_cancel); - // Trim the slices. - std::vector support_layer_storage(move_bounds.size()); - tbb::parallel_for(tbb::blocked_range(0, slices.size()), - [&](const tbb::blocked_range &range) { - for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) - if (ExPolygons &src = slices[layer_idx]; ! src.empty()) - support_layer_storage[layer_idx] = diff_clipped(to_polygons(std::move(src)), volumes.getCollision(0, layer_idx, true)); }); - std::vector support_roof_storage(move_bounds.size()); - finalize_interface_and_support_areas(print_object, volumes, config, overhangs, support_layer_storage, support_roof_storage, - bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel); + tbb::parallel_for(tbb::blocked_range(0, trees.size()), + [&trees, &throw_on_cancel](const tbb::blocked_range &range) { + for (size_t tree_id = range.begin(); tree_id < range.end(); ++ tree_id) { + Tree &tree = trees[tree_id]; + for (Slice &slice : tree.slices) + if (slice.num_branches > 1) { + slice.polygons = union_(slice.polygons); + slice.num_branches = 1; + } + throw_on_cancel(); + } + }); + + size_t num_layers = 0; + for (Tree &tree : trees) + if (tree.first_layer_id >= 0) + num_layers = std::max(num_layers, size_t(tree.first_layer_id + tree.slices.size())); + + std::vector slices(num_layers, Slice{}); + for (Tree &tree : trees) + if (tree.first_layer_id >= 0) { + for (LayerIndex i = tree.first_layer_id; i != tree.first_layer_id + LayerIndex(tree.slices.size()); ++ i) + if (Slice &src = tree.slices[i - tree.first_layer_id]; ! src.polygons.empty()) { + Slice &dst = slices[i]; + if (++ dst.num_branches > 1) + append(dst.polygons, std::move(src.polygons)); + else + dst.polygons = std::move(src.polygons); + } + } + + std::vector support_layer_storage(move_bounds.size()); + tbb::parallel_for(tbb::blocked_range(0, std::min(move_bounds.size(), slices.size())), + [&slices, &support_layer_storage, &throw_on_cancel](const tbb::blocked_range &range) { + for (size_t slice_id = range.begin(); slice_id < range.end(); ++ slice_id) { + Slice &slice = slices[slice_id]; + support_layer_storage[slice_id] = slice.num_branches > 1 ? union_(slice.polygons) : std::move(slice.polygons); + throw_on_cancel(); + } + }); + + //FIXME simplify! + return support_layer_storage; } /*! @@ -4413,10 +4483,9 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel); else { assert(print_object.config().support_material_style == smsOrganic); - indexed_triangle_set branches = draw_branches(*print.get_object(processing.second.front()), volumes, config, move_bounds, throw_on_cancel); - // Reduce memory footprint. After this point only slice_branches() will use volumes and from that only collisions with zero radius will be used. - volumes.clear_all_but_object_collision(); - slice_branches(*print.get_object(processing.second.front()), volumes, config, overhangs, move_bounds, branches, + std::vector support_layer_storage = draw_branches(*print.get_object(processing.second.front()), volumes, config, move_bounds, throw_on_cancel); + std::vector support_roof_storage(support_layer_storage.size()); + finalize_interface_and_support_areas(print_object, volumes, config, overhangs, support_layer_storage, support_roof_storage, bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel); } From 9e56625287f2563ad974a266593821d49af5efbd Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 19 Apr 2023 09:55:45 +0200 Subject: [PATCH 031/115] Fix for #10319 - MacOS 2.6 Alpha6 Crash on project load This issue was not related to the OS and was caused by bug inside export function. Wrong object id was saved for the cut objects --- src/libslic3r/Format/3mf.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index 1b3a95d1c..d08e3be31 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -2958,9 +2958,9 @@ namespace Slic3r { unsigned int object_cnt = 0; for (const ModelObject* object : model.objects) { + object_cnt++; if (!object->is_cut()) continue; - object_cnt++; pt::ptree& obj_tree = tree.add("objects.object", ""); obj_tree.put(".id", object_cnt); From 85dd2e486ab22e9d822036749efd75f0e675ccc9 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 19 Apr 2023 11:45:37 +0200 Subject: [PATCH 032/115] SPE-1667 - Added 'Zoom to mouse cursor' by Shift+Mouse wheel Successfully enhanced and integrated into PrusaSlicer from https://github.com/bambulab/BambuStudio/commit/3f2ee4062b82d8f65891bc211f590baaf0eaceee Co-authored-by: liz.li Co-authored-by: lane.wei --- src/slic3r/GUI/GLCanvas3D.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index f55f16bbc..c486e6442 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2939,8 +2939,21 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) return; // Calculate the zoom delta and apply it to the current zoom factor - double direction_factor = wxGetApp().app_config->get_bool("reverse_mouse_wheel_zoom") ? -1.0 : 1.0; - _update_camera_zoom(direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta()); + const double direction_factor = wxGetApp().app_config->get_bool("reverse_mouse_wheel_zoom") ? -1.0 : 1.0; + const double delta = direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta(); + if (wxGetKeyState(WXK_SHIFT)) { + const auto cnv_size = get_canvas_size(); + const auto screen_center_3d_pos = _mouse_to_3d({ cnv_size.get_width() * 0.5, cnv_size.get_height() * 0.5 }); + const auto mouse_3d_pos = _mouse_to_3d({ evt.GetX(), evt.GetY() }); + const Vec3d displacement = mouse_3d_pos - screen_center_3d_pos; + wxGetApp().plater()->get_camera().translate_world(displacement); + const double origin_zoom = wxGetApp().plater()->get_camera().get_zoom(); + _update_camera_zoom(delta); + const double new_zoom = wxGetApp().plater()->get_camera().get_zoom(); + wxGetApp().plater()->get_camera().translate_world((-displacement) / (new_zoom / origin_zoom)); + } + else + _update_camera_zoom(delta); } void GLCanvas3D::on_timer(wxTimerEvent& evt) From 83a2dc3b5a74c65a15447abfcc5daccf82e1b210 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 19 Apr 2023 12:06:16 +0200 Subject: [PATCH 033/115] Follow up https://github.com/Prusa-Development/PrusaSlicerPrivate/commit/9e56625287f2563ad974a266593821d49af5efbd - Added code to avoid a crash and allow to load 3mf even if cut information was corrupted --- src/libslic3r/Format/3mf.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/Format/3mf.cpp b/src/libslic3r/Format/3mf.cpp index d08e3be31..f7f68f43f 100644 --- a/src/libslic3r/Format/3mf.cpp +++ b/src/libslic3r/Format/3mf.cpp @@ -867,9 +867,12 @@ namespace Slic3r { IdToCutObjectInfoMap::iterator cut_object_info = m_cut_object_infos.find(object.second + 1); if (cut_object_info != m_cut_object_infos.end()) { model_object->cut_id = cut_object_info->second.id; - + int vol_cnt = int(model_object->volumes.size()); for (auto connector : cut_object_info->second.connectors) { - assert(0 <= connector.volume_id && connector.volume_id <= int(model_object->volumes.size())); + if (connector.volume_id < 0 || connector.volume_id >= vol_cnt) { + add_error("Invalid connector is found"); + continue; + } model_object->volumes[connector.volume_id]->cut_info = ModelVolume::CutInfo(CutConnectorType(connector.type), connector.r_tolerance, connector.h_tolerance, true); } From 99f3a3d54f039d41301c6d9647bc5e32d995eff6 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 6 Apr 2023 15:35:14 +0200 Subject: [PATCH 034/115] SPE-1649 - Fixed crash in G-code post processor calculating the preheat / cooldown positions --- src/libslic3r/GCode.cpp | 1 + src/libslic3r/GCode/GCodeProcessor.cpp | 16 ++++++++++++++-- src/libslic3r/GCode/GCodeProcessor.hpp | 6 ++++++ src/libslic3r/Print.hpp | 2 ++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8d0c7f070..29316a9de 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -836,6 +836,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu path_tmp += ".tmp"; m_processor.initialize(path_tmp); + m_processor.set_print(print); GCodeOutputStream file(boost::nowide::fopen(path_tmp.c_str(), "wb"), m_processor); if (! file.is_open()) throw Slic3r::RuntimeError(std::string("G-code export to ") + path + " failed.\nCannot open the file for writing.\n"); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 728d381f1..e34e4e5dd 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3384,7 +3384,7 @@ void GCodeProcessor::process_T(const std::string_view command) if (m_extruder_id != id) { if (((m_producer == EProducer::PrusaSlicer || m_producer == EProducer::Slic3rPE || m_producer == EProducer::Slic3r) && id >= m_result.extruders_count) || ((m_producer != EProducer::PrusaSlicer && m_producer != EProducer::Slic3rPE && m_producer != EProducer::Slic3r) && id >= m_result.extruder_colors.size())) - BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange, maybe from a custom gcode."; + BOOST_LOG_TRIVIAL(error) << "GCodeProcessor encountered an invalid toolchange, maybe from a custom gcode (" << command << ")."; else { unsigned char old_extruder_id = m_extruder_id; process_filaments(CustomGCode::ToolChange); @@ -3984,13 +3984,25 @@ void GCodeProcessor::post_process() #if ENABLE_GCODE_POSTPROCESS_BACKTRACE // add lines XXX to exported gcode - auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { + auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); if (cmd.size() >= 2) { std::stringstream ss(cmd.substr(1)); int tool_number = -1; ss >> tool_number; if (tool_number != -1) + if (tool_number < 0 || (int)m_extruder_temps_config.size() <= tool_number) { + // found an invalid value, clamp it to a valid one + tool_number = std::clamp(0, m_extruder_temps_config.size() - 1, tool_number); + // emit warning + std::string warning = _u8L("GCode Post-Processor encountered an invalid toolchange, maybe from a custom gcode:"); + warning += "\n> "; + warning += gcode_line; + warning += _u8L("Generated M104 lines may be incorrect."); + BOOST_LOG_TRIVIAL(error) << warning; + if (m_print != nullptr) + m_print->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, warning); + } export_lines.insert_lines(backtrace, cmd, // line inserter [tool_number, this](unsigned int id, float time, float time_diff) { diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 856d5b31f..1f29488c3 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -16,6 +16,8 @@ namespace Slic3r { + class Print; + enum class EMoveType : unsigned char { Noop, @@ -588,6 +590,8 @@ namespace Slic3r { TimeProcessor m_time_processor; UsedFilaments m_used_filaments; + Print* m_print{ nullptr }; + GCodeProcessorResult m_result; static unsigned int s_result_id; @@ -601,6 +605,8 @@ namespace Slic3r { GCodeProcessor(); void apply_config(const PrintConfig& config); + void set_print(Print* print) { m_print = print; } + void enable_stealth_time_estimator(bool enabled); bool is_stealth_time_estimator_enabled() const { return m_time_processor.machines[static_cast(PrintEstimatedStatistics::ETimeMode::Stealth)].enabled; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 4496de5bf..d222fbfe2 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -660,6 +660,8 @@ private: // To allow GCode to set the Print's GCodeExport step status. friend class GCode; + // To allow GCodeProcessor to emit warnings. + friend class GCodeProcessor; // Allow PrintObject to access m_mutex and m_cancel_callback. friend class PrintObject; }; From fb2448fbe399b0a4f7e63a21496d39f5dc438221 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 19 Apr 2023 13:10:39 +0200 Subject: [PATCH 035/115] Fix of missing AppConfig::has_section before get_section in ConfigWizard. --- src/slic3r/GUI/ConfigWizard.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index e38b3f194..11b116a05 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -3171,6 +3171,8 @@ bool ConfigWizard::priv::apply_config(AppConfig *app_config, PresetBundle *prese } else { auto changed = [app_config, &appconfig_new = std::as_const(this->appconfig_new)](const std::string& section_name) { + if (!appconfig_new.has_section(section_name)) + return false; return (app_config->has_section(section_name) ? app_config->get_section(section_name) : std::map()) != appconfig_new.get_section(section_name); }; bool is_filaments_changed = changed(AppConfig::SECTION_FILAMENTS); From b0cc0e98fa891fa49a7d2f94bd382dbfdbbe95c3 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 4 Apr 2023 12:47:02 +0200 Subject: [PATCH 036/115] Use the same colors as in 3D view when generating thumbnails --- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index c486e6442..d04fc7f98 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -4469,7 +4469,7 @@ void GLCanvas3D::_render_thumbnail_internal(ThumbnailData& thumbnail_data, const const Transform3d& projection_matrix = camera.get_projection_matrix(); for (GLVolume* vol : visible_volumes) { - vol->model.set_color((vol->printable && !vol->is_outside) ? (current_printer_technology() == ptSLA ? vol->color : ColorRGBA::ORANGE()) : ColorRGBA::GRAY()); + vol->model.set_color((vol->printable && !vol->is_outside) ? vol->color : ColorRGBA::GRAY()); // the volume may have been deactivated by an active gizmo const bool is_active = vol->is_active; vol->is_active = true; From 88f4fa20df3e73b962b7800cc709b0ed43113338 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 19 Apr 2023 15:58:25 +0200 Subject: [PATCH 037/115] Fix for SPE-1618 - Place on face gizmo doesn't work correctly with instances --- src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp | 9 ++++++--- src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp index 398aebb52..98058ee17 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -60,11 +60,13 @@ void GLGizmoFlatten::data_changed(bool is_serializing) { const Selection & selection = m_parent.get_selection(); const ModelObject *model_object = nullptr; + int instance_id = -1; if (selection.is_single_full_instance() || selection.is_from_single_object() ) { model_object = selection.get_model()->objects[selection.get_object_idx()]; + instance_id = selection.get_instance_idx(); } - set_flattening_data(model_object); + set_flattening_data(model_object, instance_id); } bool GLGizmoFlatten::on_init() @@ -156,9 +158,9 @@ void GLGizmoFlatten::on_unregister_raycasters_for_picking() m_planes_casters.clear(); } -void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object) +void GLGizmoFlatten::set_flattening_data(const ModelObject* model_object, int instance_id) { - if (model_object != m_old_model_object) { + if (model_object != m_old_model_object || instance_id != m_old_instance_id) { m_planes.clear(); on_unregister_raycasters_for_picking(); } @@ -363,6 +365,7 @@ void GLGizmoFlatten::update_planes() m_first_instance_scale = mo->instances.front()->get_scaling_factor(); m_first_instance_mirror = mo->instances.front()->get_mirror(); m_old_model_object = mo; + m_old_instance_id = m_c->selection_info()->get_active_instance(); // And finally create respective VBOs. The polygon is convex with // the vertices in order, so triangulation is trivial. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp index 1701b76a5..ff44fcd2d 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp @@ -39,6 +39,7 @@ private: std::vector> m_planes_casters; bool m_mouse_left_down = false; // for detection left_up of this gizmo const ModelObject* m_old_model_object = nullptr; + int m_old_instance_id{ -1 }; void update_planes(); bool is_plane_update_necessary() const; @@ -46,7 +47,7 @@ private: public: GLGizmoFlatten(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id); - void set_flattening_data(const ModelObject* model_object); + void set_flattening_data(const ModelObject* model_object, int instance_id); ///

/// Apply rotation on select plane From 9efed4be225615b3fac420bc2993cd0cb1e3429c Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 12 Apr 2023 15:08:00 +0200 Subject: [PATCH 038/115] SPE-1438 - Reset mouse dragging state when entering the 3D scene after the user released the mouse buttons outside of it --- src/slic3r/GUI/GLCanvas3D.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index d04fc7f98..72aaa5944 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3189,6 +3189,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_canvas->SetFocus(); if (evt.Entering()) { + if (m_mouse.dragging && !evt.LeftIsDown() && !evt.RightIsDown() && !evt.MiddleIsDown()) + // reset dragging state if the user released the mouse button outside the 3D scene + m_mouse.dragging = false; + //#if defined(__WXMSW__) || defined(__linux__) // // On Windows and Linux needs focus in order to catch key events // Set focus in order to remove it from object list From 5c581e3998bd37a38adaf714314bfec75e7bc116 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 20 Apr 2023 08:42:09 +0200 Subject: [PATCH 039/115] Fixed warnings --- src/libslic3r/GCode/GCodeProcessor.cpp | 4 ++-- src/slic3r/GUI/3DScene.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp | 2 +- src/slic3r/GUI/ImGuiWrapper.cpp | 2 +- src/slic3r/GUI/SceneRaycaster.cpp | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index e34e4e5dd..d0c996290 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3610,7 +3610,7 @@ void GCodeProcessor::post_process() last_time_insertion = rev_it->time; const std::string out_line = line_inserter(i + 1, last_time_insertion, m_time - last_time_insertion); rev_it_dist = std::distance(m_lines.rbegin(), rev_it) + 1; - const auto new_it = m_lines.insert(rev_it.base(), { out_line, rev_it->time }); + m_lines.insert(rev_it.base(), { out_line, rev_it->time }); #ifndef NDEBUG m_statistics.add_line(out_line.length()); #endif // NDEBUG @@ -3984,7 +3984,7 @@ void GCodeProcessor::post_process() #if ENABLE_GCODE_POSTPROCESS_BACKTRACE // add lines XXX to exported gcode - auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { + auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); if (cmd.size() >= 2) { std::stringstream ss(cmd.substr(1)); diff --git a/src/slic3r/GUI/3DScene.cpp b/src/slic3r/GUI/3DScene.cpp index 97975404f..b4e3936da 100644 --- a/src/slic3r/GUI/3DScene.cpp +++ b/src/slic3r/GUI/3DScene.cpp @@ -593,7 +593,7 @@ void GLVolumeCollection::load_object_auxiliary( return; const Transform3d mesh_trafo_inv = print_object->trafo().inverse(); - auto add_volume = [this, &instances, timestamp](int obj_idx, int inst_idx, const ModelInstance& model_instance, SLAPrintObjectStep step, + auto add_volume = [this, timestamp](int obj_idx, int inst_idx, const ModelInstance& model_instance, SLAPrintObjectStep step, const TriangleMesh& mesh, const ColorRGBA& color, std::optional convex_hull = std::nullopt) { if (mesh.empty()) return; diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp index 785c66076..e0d2cdb68 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.hpp @@ -294,7 +294,7 @@ protected: private: int m_print_object_idx = -1; - int m_print_objects_count = 0; +// int m_print_objects_count = 0; std::unique_ptr m_supports_clipper; std::unique_ptr m_pad_clipper; }; diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 4cce6e263..0754c35cc 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -460,7 +460,7 @@ bool ImGuiWrapper::button(const wxString &label, const wxString& tooltip) if (!tooltip.IsEmpty() && ImGui::IsItemHovered()) { auto tooltip_utf8 = into_u8(tooltip); - ImGui::SetTooltip(tooltip_utf8.c_str()); + ImGui::SetTooltip(tooltip_utf8.c_str(), nullptr); } return ret; diff --git a/src/slic3r/GUI/SceneRaycaster.cpp b/src/slic3r/GUI/SceneRaycaster.cpp index 1f44a07d6..64493d86b 100644 --- a/src/slic3r/GUI/SceneRaycaster.cpp +++ b/src/slic3r/GUI/SceneRaycaster.cpp @@ -117,7 +117,7 @@ SceneRaycaster::HitResult SceneRaycaster::hit(const Vec2d& mouse_pos, const Came return false; if (hit.type == SceneRaycaster::EType::Volume) - m_selected_volume_already_found = *m_selected_volume_id == decode_id(hit.type, hit.raycaster_id); + m_selected_volume_already_found = *m_selected_volume_id == (unsigned int)decode_id(hit.type, hit.raycaster_id); m_closest_hit_pos = hit.position; return true; From 9cde96993e9f996b44f417570ba05455472efa08 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 20 Apr 2023 11:31:44 +0200 Subject: [PATCH 040/115] Reworked the ClipperLib / Polygon types to use the tbb::scallable_allocator to better scale on multiple threads. --- CMakeLists.txt | 1 + src/clipper/CMakeLists.txt | 2 + src/clipper/clipper.cpp | 10 +- src/clipper/clipper.hpp | 40 +++-- src/libnest2d/CMakeLists.txt | 2 +- .../Arachne/SkeletalTrapezoidation.cpp | 8 +- .../Arachne/SkeletalTrapezoidation.hpp | 2 +- src/libslic3r/Arachne/utils/VoronoiUtils.cpp | 4 +- src/libslic3r/Arachne/utils/VoronoiUtils.hpp | 2 +- src/libslic3r/BoundingBox.cpp | 147 ++++++++---------- src/libslic3r/BoundingBox.hpp | 125 +++++++-------- src/libslic3r/CMakeLists.txt | 1 + src/libslic3r/ClipperUtils.cpp | 12 +- src/libslic3r/ClipperZUtils.hpp | 8 +- src/libslic3r/EdgeGrid.hpp | 2 +- src/libslic3r/ExPolygon.cpp | 4 +- src/libslic3r/GCode/ExtrusionProcessor.hpp | 6 +- src/libslic3r/GCode/SeamPlacer.cpp | 2 +- src/libslic3r/Geometry.hpp | 5 - src/libslic3r/JumpPointSearch.cpp | 6 +- src/libslic3r/MultiPoint.cpp | 4 +- src/libslic3r/MultiPoint.hpp | 2 +- src/libslic3r/Point.cpp | 8 +- src/libslic3r/Point.hpp | 23 +-- src/libslic3r/Polygon.cpp | 2 +- src/libslic3r/Polygon.hpp | 5 +- src/libslic3r/PolygonTrimmer.hpp | 2 +- src/libslic3r/Polyline.hpp | 5 +- src/libslic3r/Print.cpp | 4 +- src/libslic3r/Print.hpp | 2 +- src/libslic3r/PrintObject.cpp | 4 +- src/libslic3r/SLA/ConcaveHull.cpp | 3 +- src/libslic3r/ShortestPath.hpp | 9 +- src/libslic3r/libslic3r.h | 8 +- tests/libnest2d/CMakeLists.txt | 2 +- tests/libslic3r/test_geometry.cpp | 26 ++-- tests/sla_print/CMakeLists.txt | 2 +- tests/slic3rutils/CMakeLists.txt | 2 +- 38 files changed, 261 insertions(+), 241 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e19d3147b..66b9a777b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -406,6 +406,7 @@ endif() set(TBB_DEBUG 1) find_package(TBB REQUIRED) slic3r_remap_configs(TBB::tbb RelWithDebInfo Release) +slic3r_remap_configs(TBB::tbbmalloc RelWithDebInfo Release) # include_directories(${TBB_INCLUDE_DIRS}) # add_definitions(${TBB_DEFINITIONS}) # if(MSVC) diff --git a/src/clipper/CMakeLists.txt b/src/clipper/CMakeLists.txt index 3cb7cb6bb..1c1cfd5a7 100644 --- a/src/clipper/CMakeLists.txt +++ b/src/clipper/CMakeLists.txt @@ -8,3 +8,5 @@ add_library(clipper STATIC clipper_z.cpp clipper_z.hpp ) + +target_link_libraries(clipper TBB::tbb TBB::tbbmalloc) diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index c4b7e8bc2..c775a3226 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -784,7 +784,7 @@ bool ClipperBase::AddPath(const Path &pg, PolyType PolyTyp, bool Closed) return false; // Allocate a new edge array. - std::vector edges(highI + 1); + Edges edges(highI + 1); // Fill in the edge array. bool result = AddPathInternal(pg, highI, PolyTyp, Closed, edges.data()); if (result) @@ -1079,7 +1079,7 @@ Clipper::Clipper(int initOptions) : void Clipper::Reset() { ClipperBase::Reset(); - m_Scanbeam = std::priority_queue(); + m_Scanbeam = std::priority_queue{}; m_Maxima.clear(); m_ActiveEdges = 0; m_SortedEdges = 0; @@ -2226,8 +2226,8 @@ void Clipper::ProcessHorizontal(TEdge *horzEdge) if (!eLastHorz->NextInLML) eMaxPair = GetMaximaPair(eLastHorz); - std::vector::const_iterator maxIt; - std::vector::const_reverse_iterator maxRit; + cInts::const_iterator maxIt; + cInts::const_reverse_iterator maxRit; if (!m_Maxima.empty()) { //get the first maxima in range (X) ... @@ -3941,7 +3941,7 @@ void CleanPolygon(const Path& in_poly, Path& out_poly, double distance) return; } - std::vector outPts(size); + OutPts outPts(size); for (size_t i = 0; i < size; ++i) { outPts[i].Pt = in_poly[i]; diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index 849672a8f..a2a6712b2 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -39,6 +39,8 @@ #include +#include + #define CLIPPER_VERSION "6.2.6" //CLIPPERLIB_USE_XYZ: adds a Z member to IntPoint. Adds a minor cost to perfomance. @@ -112,8 +114,10 @@ using DoublePoint = Eigen::Matrix; //------------------------------------------------------------------------------ -typedef std::vector Path; -typedef std::vector Paths; +template +using Allocator = tbb::scalable_allocator; +using Path = std::vector>; +using Paths = std::vector>; inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;} inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;} @@ -133,7 +137,7 @@ enum JoinType {jtSquare, jtRound, jtMiter}; enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound}; class PolyNode; -typedef std::vector< PolyNode* > PolyNodes; +typedef std::vector> PolyNodes; class PolyNode { @@ -186,7 +190,7 @@ public: private: PolyTree(const PolyTree &src) = delete; PolyTree& operator=(const PolyTree &src) = delete; - std::vector AllNodes; + std::vector> AllNodes; friend class Clipper; //to access AllNodes }; @@ -277,6 +281,8 @@ enum EdgeSide { esLeft = 1, esRight = 2}; OutPt *Prev; }; + using OutPts = std::vector>; + struct OutRec; struct Join { Join(OutPt *OutPt1, OutPt *OutPt2, IntPoint OffPt) : @@ -312,7 +318,7 @@ public: if (num_paths == 1) return AddPath(*paths_provider.begin(), PolyTyp, Closed); - std::vector num_edges(num_paths, 0); + std::vector> num_edges(num_paths, 0); int num_edges_total = 0; size_t i = 0; for (const Path &pg : paths_provider) { @@ -333,7 +339,7 @@ public: return false; // Allocate a new edge array. - std::vector edges(num_edges_total); + std::vector> edges(num_edges_total); // Fill in the edge array. bool result = false; TEdge *p_edge = edges.data(); @@ -369,7 +375,7 @@ protected: void AscendToMax(TEdge *&E, bool Appending, bool IsClosed); // Local minima (Y, left edge, right edge) sorted by ascending Y. - std::vector m_MinimaList; + std::vector> m_MinimaList; #ifdef CLIPPERLIB_INT32 static constexpr const bool m_UseFullRange = false; @@ -380,7 +386,8 @@ protected: #endif // CLIPPERLIB_INT32 // A vector of edges per each input path. - std::vector> m_edges; + using Edges = std::vector>; + std::vector> m_edges; // Don't remove intermediate vertices of a collinear sequence of points. bool m_PreserveCollinear; // Is any of the paths inserted by AddPath() or AddPaths() open? @@ -424,22 +431,23 @@ protected: private: // Output polygons. - std::vector m_PolyOuts; + std::vector> m_PolyOuts; // Output points, allocated by a continuous sets of m_OutPtsChunkSize. - std::vector m_OutPts; + std::vector> m_OutPts; // List of free output points, to be used before taking a point from m_OutPts or allocating a new chunk. OutPt *m_OutPtsFree; size_t m_OutPtsChunkSize; size_t m_OutPtsChunkLast; - std::vector m_Joins; - std::vector m_GhostJoins; - std::vector m_IntersectList; + std::vector> m_Joins; + std::vector> m_GhostJoins; + std::vector> m_IntersectList; ClipType m_ClipType; // A priority queue (a binary heap) of Y coordinates. - std::priority_queue m_Scanbeam; + using cInts = std::vector>; + std::priority_queue m_Scanbeam; // Maxima are collected by ProcessEdgesAtTopOfScanbeam(), consumed by ProcessHorizontal(). - std::vector m_Maxima; + cInts m_Maxima; TEdge *m_ActiveEdges; TEdge *m_SortedEdges; PolyFillType m_ClipFillType; @@ -530,7 +538,7 @@ private: Paths m_destPolys; Path m_srcPoly; Path m_destPoly; - std::vector m_normals; + std::vector> m_normals; double m_delta, m_sinA, m_sin, m_cos; double m_miterLim, m_StepsPerRad; // x: index of the lowest contour in m_polyNodes diff --git a/src/libnest2d/CMakeLists.txt b/src/libnest2d/CMakeLists.txt index c18dc31cb..154c965e5 100644 --- a/src/libnest2d/CMakeLists.txt +++ b/src/libnest2d/CMakeLists.txt @@ -24,5 +24,5 @@ set(LIBNEST2D_SRCFILES add_library(libnest2d STATIC ${LIBNEST2D_SRCFILES}) target_include_directories(libnest2d PUBLIC ${CMAKE_CURRENT_LIST_DIR}/include) -target_link_libraries(libnest2d PUBLIC NLopt::nlopt TBB::tbb Boost::boost libslic3r) +target_link_libraries(libnest2d PUBLIC NLopt::nlopt TBB::tbb TBB::tbbmalloc Boost::boost libslic3r) target_compile_definitions(libnest2d PUBLIC LIBNEST2D_THREADING_tbb LIBNEST2D_STATIC LIBNEST2D_OPTIMIZER_nlopt LIBNEST2D_GEOMETRIES_libslic3r) diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 381f18b70..a73a4918a 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -181,7 +181,7 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, vd_t::edge_type& } else { - std::vector discretized = discretize(vd_edge, segments); + Points discretized = discretize(vd_edge, segments); assert(discretized.size() >= 2); if(discretized.size() < 2) { @@ -236,7 +236,7 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, vd_t::edge_type& } } -std::vector SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_edge, const std::vector& segments) +Points SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_edge, const std::vector& segments) { /*Terminology in this function assumes that the edge moves horizontally from left to right. This is not necessarily the case; the edge can go in any @@ -257,7 +257,7 @@ std::vector SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_ bool point_right = right_cell->contains_point(); if ((!point_left && !point_right) || vd_edge.is_secondary()) // Source vert is directly connected to source segment { - return std::vector({ start, end }); + return Points({ start, end }); } else if (point_left != point_right) //This is a parabolic edge between a point and a line. { @@ -311,7 +311,7 @@ std::vector SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_ //Start generating points along the edge. Point a = start; Point b = end; - std::vector ret; + Points ret; ret.emplace_back(a); //Introduce an extra edge at the borders of the markings? diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index b4029d586..7b8ecf834 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -204,7 +204,7 @@ protected: * \return A number of coordinates along the edge where the edge is broken * up into discrete pieces. */ - std::vector discretize(const vd_t::edge_type& segment, const std::vector& segments); + Points discretize(const vd_t::edge_type& segment, const std::vector& segments); /*! * Compute the range of line segments that surround a cell of the skeletal diff --git a/src/libslic3r/Arachne/utils/VoronoiUtils.cpp b/src/libslic3r/Arachne/utils/VoronoiUtils.cpp index 069e1f5ad..675a0ebb4 100644 --- a/src/libslic3r/Arachne/utils/VoronoiUtils.cpp +++ b/src/libslic3r/Arachne/utils/VoronoiUtils.cpp @@ -138,9 +138,9 @@ public: return Point(coord_t(p.x() * matrix[0] + p.y() * matrix[2]), coord_t(p.x() * matrix[1] + p.y() * matrix[3])); } }; -std::vector VoronoiUtils::discretizeParabola(const Point& p, const Segment& segment, Point s, Point e, coord_t approximate_step_size, float transitioning_angle) +Points VoronoiUtils::discretizeParabola(const Point& p, const Segment& segment, Point s, Point e, coord_t approximate_step_size, float transitioning_angle) { - std::vector discretized; + Points discretized; // x is distance of point projected on the segment ab // xx is point projected on the segment ab const Point a = segment.from(); diff --git a/src/libslic3r/Arachne/utils/VoronoiUtils.hpp b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp index aa4693643..ea6a8495a 100644 --- a/src/libslic3r/Arachne/utils/VoronoiUtils.hpp +++ b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp @@ -34,7 +34,7 @@ public: * Discretize a parabola based on (approximate) step size. * The \p approximate_step_size is measured parallel to the \p source_segment, not along the parabola. */ - static std::vector discretizeParabola(const Point &source_point, const Segment &source_segment, Point start, Point end, coord_t approximate_step_size, float transitioning_angle); + static Points discretizeParabola(const Point &source_point, const Segment &source_segment, Point start, Point end, coord_t approximate_step_size, float transitioning_angle); static inline bool is_finite(const VoronoiUtils::vd_t::vertex_type &vertex) { diff --git a/src/libslic3r/BoundingBox.cpp b/src/libslic3r/BoundingBox.cpp index 4f52c5108..10543fc95 100644 --- a/src/libslic3r/BoundingBox.cpp +++ b/src/libslic3r/BoundingBox.cpp @@ -6,23 +6,19 @@ namespace Slic3r { -template BoundingBoxBase::BoundingBoxBase(const std::vector &points); +template BoundingBoxBase::BoundingBoxBase(const Points &points); template BoundingBoxBase::BoundingBoxBase(const std::vector &points); template BoundingBox3Base::BoundingBox3Base(const std::vector &points); void BoundingBox::polygon(Polygon* polygon) const { - polygon->points.clear(); - polygon->points.resize(4); - polygon->points[0](0) = this->min(0); - polygon->points[0](1) = this->min(1); - polygon->points[1](0) = this->max(0); - polygon->points[1](1) = this->min(1); - polygon->points[2](0) = this->max(0); - polygon->points[2](1) = this->max(1); - polygon->points[3](0) = this->min(0); - polygon->points[3](1) = this->max(1); + polygon->points = { + this->min, + { this->max.x(), this->min.y() }, + this->max, + { this->min.x(), this->max.y() } + }; } Polygon BoundingBox::polygon() const @@ -37,8 +33,8 @@ BoundingBox BoundingBox::rotated(double angle) const BoundingBox out; out.merge(this->min.rotated(angle)); out.merge(this->max.rotated(angle)); - out.merge(Point(this->min(0), this->max(1)).rotated(angle)); - out.merge(Point(this->max(0), this->min(1)).rotated(angle)); + out.merge(Point(this->min.x(), this->max.y()).rotated(angle)); + out.merge(Point(this->max.x(), this->min.y()).rotated(angle)); return out; } @@ -47,23 +43,23 @@ BoundingBox BoundingBox::rotated(double angle, const Point ¢er) const BoundingBox out; out.merge(this->min.rotated(angle, center)); out.merge(this->max.rotated(angle, center)); - out.merge(Point(this->min(0), this->max(1)).rotated(angle, center)); - out.merge(Point(this->max(0), this->min(1)).rotated(angle, center)); + out.merge(Point(this->min.x(), this->max.y()).rotated(angle, center)); + out.merge(Point(this->max.x(), this->min.y()).rotated(angle, center)); return out; } -template void -BoundingBoxBase::scale(double factor) +template void +BoundingBoxBase::scale(double factor) { this->min *= factor; this->max *= factor; } -template void BoundingBoxBase::scale(double factor); +template void BoundingBoxBase::scale(double factor); template void BoundingBoxBase::scale(double factor); template void BoundingBoxBase::scale(double factor); -template void -BoundingBoxBase::merge(const PointClass &point) +template void +BoundingBoxBase::merge(const PointType &point) { if (this->defined) { this->min = this->min.cwiseMin(point); @@ -74,22 +70,22 @@ BoundingBoxBase::merge(const PointClass &point) this->defined = true; } } -template void BoundingBoxBase::merge(const Point &point); +template void BoundingBoxBase::merge(const Point &point); template void BoundingBoxBase::merge(const Vec2f &point); template void BoundingBoxBase::merge(const Vec2d &point); -template void -BoundingBoxBase::merge(const std::vector &points) +template void +BoundingBoxBase::merge(const PointsType &points) { this->merge(BoundingBoxBase(points)); } -template void BoundingBoxBase::merge(const Points &points); +template void BoundingBoxBase::merge(const Points &points); template void BoundingBoxBase::merge(const Pointfs &points); -template void -BoundingBoxBase::merge(const BoundingBoxBase &bb) +template void +BoundingBoxBase::merge(const BoundingBoxBase &bb) { - assert(bb.defined || bb.min(0) >= bb.max(0) || bb.min(1) >= bb.max(1)); + assert(bb.defined || bb.min.x() >= bb.max.x() || bb.min.y() >= bb.max.y()); if (bb.defined) { if (this->defined) { this->min = this->min.cwiseMin(bb.min); @@ -101,12 +97,12 @@ BoundingBoxBase::merge(const BoundingBoxBase &bb) } } } -template void BoundingBoxBase::merge(const BoundingBoxBase &bb); +template void BoundingBoxBase::merge(const BoundingBoxBase &bb); template void BoundingBoxBase::merge(const BoundingBoxBase &bb); template void BoundingBoxBase::merge(const BoundingBoxBase &bb); -template void -BoundingBox3Base::merge(const PointClass &point) +template void +BoundingBox3Base::merge(const PointType &point) { if (this->defined) { this->min = this->min.cwiseMin(point); @@ -120,17 +116,17 @@ BoundingBox3Base::merge(const PointClass &point) template void BoundingBox3Base::merge(const Vec3f &point); template void BoundingBox3Base::merge(const Vec3d &point); -template void -BoundingBox3Base::merge(const std::vector &points) +template void +BoundingBox3Base::merge(const PointsType &points) { this->merge(BoundingBox3Base(points)); } template void BoundingBox3Base::merge(const Pointf3s &points); -template void -BoundingBox3Base::merge(const BoundingBox3Base &bb) +template void +BoundingBox3Base::merge(const BoundingBox3Base &bb) { - assert(bb.defined || bb.min(0) >= bb.max(0) || bb.min(1) >= bb.max(1) || bb.min(2) >= bb.max(2)); + assert(bb.defined || bb.min.x() >= bb.max.x() || bb.min.y() >= bb.max.y() || bb.min.z() >= bb.max.z()); if (bb.defined) { if (this->defined) { this->min = this->min.cwiseMin(bb.min); @@ -144,83 +140,78 @@ BoundingBox3Base::merge(const BoundingBox3Base &bb) } template void BoundingBox3Base::merge(const BoundingBox3Base &bb); -template PointClass -BoundingBoxBase::size() const +template PointType +BoundingBoxBase::size() const { - return PointClass(this->max(0) - this->min(0), this->max(1) - this->min(1)); + return this->max - this->min; } -template Point BoundingBoxBase::size() const; +template Point BoundingBoxBase::size() const; template Vec2f BoundingBoxBase::size() const; template Vec2d BoundingBoxBase::size() const; -template PointClass -BoundingBox3Base::size() const +template PointType +BoundingBox3Base::size() const { - return PointClass(this->max(0) - this->min(0), this->max(1) - this->min(1), this->max(2) - this->min(2)); + return this->max - this->min; } template Vec3f BoundingBox3Base::size() const; template Vec3d BoundingBox3Base::size() const; -template double BoundingBoxBase::radius() const +template double BoundingBoxBase::radius() const { assert(this->defined); - double x = this->max(0) - this->min(0); - double y = this->max(1) - this->min(1); - return 0.5 * sqrt(x*x+y*y); + return 0.5 * (this->max - this->min).cast().norm(); } -template double BoundingBoxBase::radius() const; +template double BoundingBoxBase::radius() const; template double BoundingBoxBase::radius() const; -template double BoundingBox3Base::radius() const +template double BoundingBox3Base::radius() const { - double x = this->max(0) - this->min(0); - double y = this->max(1) - this->min(1); - double z = this->max(2) - this->min(2); - return 0.5 * sqrt(x*x+y*y+z*z); + return 0.5 * (this->max - this->min).cast().norm(); } template double BoundingBox3Base::radius() const; -template void -BoundingBoxBase::offset(coordf_t delta) +template void +BoundingBoxBase::offset(coordf_t delta) { - PointClass v(delta, delta); + PointType v(delta, delta); this->min -= v; this->max += v; } -template void BoundingBoxBase::offset(coordf_t delta); +template void BoundingBoxBase::offset(coordf_t delta); template void BoundingBoxBase::offset(coordf_t delta); -template void -BoundingBox3Base::offset(coordf_t delta) +template void +BoundingBox3Base::offset(coordf_t delta) { - PointClass v(delta, delta, delta); + PointType v(delta, delta, delta); this->min -= v; this->max += v; } template void BoundingBox3Base::offset(coordf_t delta); -template PointClass -BoundingBoxBase::center() const +template PointType +BoundingBoxBase::center() const { return (this->min + this->max) / 2; } -template Point BoundingBoxBase::center() const; +template Point BoundingBoxBase::center() const; template Vec2f BoundingBoxBase::center() const; template Vec2d BoundingBoxBase::center() const; -template PointClass -BoundingBox3Base::center() const +template PointType +BoundingBox3Base::center() const { return (this->min + this->max) / 2; } template Vec3f BoundingBox3Base::center() const; template Vec3d BoundingBox3Base::center() const; -template coordf_t -BoundingBox3Base::max_size() const +template coordf_t +BoundingBox3Base::max_size() const { - PointClass s = size(); - return std::max(s(0), std::max(s(1), s(2))); + PointType s = size(); + return std::max(s.x(), std::max(s.y(), s.z())); } template coordf_t BoundingBox3Base::max_size() const; template coordf_t BoundingBox3Base::max_size() const; @@ -228,8 +219,8 @@ template coordf_t BoundingBox3Base::max_size() const; void BoundingBox::align_to_grid(const coord_t cell_size) { if (this->defined) { - min(0) = Slic3r::align_to_grid(min(0), cell_size); - min(1) = Slic3r::align_to_grid(min(1), cell_size); + min.x() = Slic3r::align_to_grid(min.x(), cell_size); + min.y() = Slic3r::align_to_grid(min.y(), cell_size); } } @@ -238,14 +229,14 @@ BoundingBoxf3 BoundingBoxf3::transformed(const Transform3d& matrix) const typedef Eigen::Matrix Vertices; Vertices src_vertices; - src_vertices(0, 0) = min(0); src_vertices(1, 0) = min(1); src_vertices(2, 0) = min(2); - src_vertices(0, 1) = max(0); src_vertices(1, 1) = min(1); src_vertices(2, 1) = min(2); - src_vertices(0, 2) = max(0); src_vertices(1, 2) = max(1); src_vertices(2, 2) = min(2); - src_vertices(0, 3) = min(0); src_vertices(1, 3) = max(1); src_vertices(2, 3) = min(2); - src_vertices(0, 4) = min(0); src_vertices(1, 4) = min(1); src_vertices(2, 4) = max(2); - src_vertices(0, 5) = max(0); src_vertices(1, 5) = min(1); src_vertices(2, 5) = max(2); - src_vertices(0, 6) = max(0); src_vertices(1, 6) = max(1); src_vertices(2, 6) = max(2); - src_vertices(0, 7) = min(0); src_vertices(1, 7) = max(1); src_vertices(2, 7) = max(2); + src_vertices(0, 0) = min.x(); src_vertices(1, 0) = min.y(); src_vertices(2, 0) = min.z(); + src_vertices(0, 1) = max.x(); src_vertices(1, 1) = min.y(); src_vertices(2, 1) = min.z(); + src_vertices(0, 2) = max.x(); src_vertices(1, 2) = max.y(); src_vertices(2, 2) = min.z(); + src_vertices(0, 3) = min.x(); src_vertices(1, 3) = max.y(); src_vertices(2, 3) = min.z(); + src_vertices(0, 4) = min.x(); src_vertices(1, 4) = min.y(); src_vertices(2, 4) = max.z(); + src_vertices(0, 5) = max.x(); src_vertices(1, 5) = min.y(); src_vertices(2, 5) = max.z(); + src_vertices(0, 6) = max.x(); src_vertices(1, 6) = max.y(); src_vertices(2, 6) = max.z(); + src_vertices(0, 7) = min.x(); src_vertices(1, 7) = max.y(); src_vertices(2, 7) = max.z(); Vertices dst_vertices = matrix * src_vertices.colwise().homogeneous(); diff --git a/src/libslic3r/BoundingBox.hpp b/src/libslic3r/BoundingBox.hpp index d741be36c..fc1b50074 100644 --- a/src/libslic3r/BoundingBox.hpp +++ b/src/libslic3r/BoundingBox.hpp @@ -8,53 +8,54 @@ namespace Slic3r { -template +template > class BoundingBoxBase { public: - PointClass min; - PointClass max; + using PointsType = APointsType; + PointType min; + PointType max; bool defined; - BoundingBoxBase() : min(PointClass::Zero()), max(PointClass::Zero()), defined(false) {} - BoundingBoxBase(const PointClass &pmin, const PointClass &pmax) : + BoundingBoxBase() : min(PointType::Zero()), max(PointType::Zero()), defined(false) {} + BoundingBoxBase(const PointType &pmin, const PointType &pmax) : min(pmin), max(pmax), defined(pmin.x() < pmax.x() && pmin.y() < pmax.y()) {} - BoundingBoxBase(const PointClass &p1, const PointClass &p2, const PointClass &p3) : + BoundingBoxBase(const PointType &p1, const PointType &p2, const PointType &p3) : min(p1), max(p1), defined(false) { merge(p2); merge(p3); } template> BoundingBoxBase(It from, It to) { construct(*this, from, to); } - BoundingBoxBase(const std::vector &points) + BoundingBoxBase(const PointsType &points) : BoundingBoxBase(points.begin(), points.end()) {} - void reset() { this->defined = false; this->min = PointClass::Zero(); this->max = PointClass::Zero(); } - void merge(const PointClass &point); - void merge(const std::vector &points); - void merge(const BoundingBoxBase &bb); + void reset() { this->defined = false; this->min = PointType::Zero(); this->max = PointType::Zero(); } + void merge(const PointType &point); + void merge(const PointsType &points); + void merge(const BoundingBoxBase &bb); void scale(double factor); - PointClass size() const; + PointType size() const; double radius() const; - void translate(coordf_t x, coordf_t y) { assert(this->defined); PointClass v(x, y); this->min += v; this->max += v; } - void translate(const PointClass &v) { this->min += v; this->max += v; } + void translate(coordf_t x, coordf_t y) { assert(this->defined); PointType v(x, y); this->min += v; this->max += v; } + void translate(const PointType &v) { this->min += v; this->max += v; } void offset(coordf_t delta); - BoundingBoxBase inflated(coordf_t delta) const throw() { BoundingBoxBase out(*this); out.offset(delta); return out; } - PointClass center() const; - bool contains(const PointClass &point) const { + BoundingBoxBase inflated(coordf_t delta) const throw() { BoundingBoxBase out(*this); out.offset(delta); return out; } + PointType center() const; + bool contains(const PointType &point) const { return point.x() >= this->min.x() && point.x() <= this->max.x() && point.y() >= this->min.y() && point.y() <= this->max.y(); } - bool contains(const BoundingBoxBase &other) const { + bool contains(const BoundingBoxBase &other) const { return contains(other.min) && contains(other.max); } - bool overlap(const BoundingBoxBase &other) const { + bool overlap(const BoundingBoxBase &other) const { return ! (this->max.x() < other.min.x() || this->min.x() > other.max.x() || this->max.y() < other.min.y() || this->min.y() > other.max.y()); } - bool operator==(const BoundingBoxBase &rhs) { return this->min == rhs.min && this->max == rhs.max; } - bool operator!=(const BoundingBoxBase &rhs) { return ! (*this == rhs); } + bool operator==(const BoundingBoxBase &rhs) { return this->min == rhs.min && this->max == rhs.max; } + bool operator!=(const BoundingBoxBase &rhs) { return ! (*this == rhs); } private: // to access construct() @@ -69,10 +70,10 @@ private: { if (from != to) { auto it = from; - out.min = it->template cast(); + out.min = it->template cast(); out.max = out.min; for (++ it; it != to; ++ it) { - auto vec = it->template cast(); + auto vec = it->template cast(); out.min = out.min.cwiseMin(vec); out.max = out.max.cwiseMax(vec); } @@ -81,16 +82,18 @@ private: } }; -template -class BoundingBox3Base : public BoundingBoxBase +template +class BoundingBox3Base : public BoundingBoxBase> { public: - BoundingBox3Base() : BoundingBoxBase() {} - BoundingBox3Base(const PointClass &pmin, const PointClass &pmax) : - BoundingBoxBase(pmin, pmax) - { if (pmin.z() >= pmax.z()) BoundingBoxBase::defined = false; } - BoundingBox3Base(const PointClass &p1, const PointClass &p2, const PointClass &p3) : - BoundingBoxBase(p1, p1) { merge(p2); merge(p3); } + using PointsType = std::vector; + + BoundingBox3Base() : BoundingBoxBase() {} + BoundingBox3Base(const PointType &pmin, const PointType &pmax) : + BoundingBoxBase(pmin, pmax) + { if (pmin.z() >= pmax.z()) BoundingBoxBase::defined = false; } + BoundingBox3Base(const PointType &p1, const PointType &p2, const PointType &p3) : + BoundingBoxBase(p1, p1) { merge(p2); merge(p3); } template > BoundingBox3Base(It from, It to) { @@ -98,67 +101,67 @@ public: throw Slic3r::InvalidArgument("Empty point set supplied to BoundingBox3Base constructor"); auto it = from; - this->min = it->template cast(); + this->min = it->template cast(); this->max = this->min; for (++ it; it != to; ++ it) { - auto vec = it->template cast(); + auto vec = it->template cast(); this->min = this->min.cwiseMin(vec); this->max = this->max.cwiseMax(vec); } this->defined = (this->min.x() < this->max.x()) && (this->min.y() < this->max.y()) && (this->min.z() < this->max.z()); } - BoundingBox3Base(const std::vector &points) + BoundingBox3Base(const PointsType &points) : BoundingBox3Base(points.begin(), points.end()) {} - void merge(const PointClass &point); - void merge(const std::vector &points); - void merge(const BoundingBox3Base &bb); - PointClass size() const; + void merge(const PointType &point); + void merge(const PointsType &points); + void merge(const BoundingBox3Base &bb); + PointType size() const; double radius() const; - void translate(coordf_t x, coordf_t y, coordf_t z) { assert(this->defined); PointClass v(x, y, z); this->min += v; this->max += v; } + void translate(coordf_t x, coordf_t y, coordf_t z) { assert(this->defined); PointType v(x, y, z); this->min += v; this->max += v; } void translate(const Vec3d &v) { this->min += v; this->max += v; } void offset(coordf_t delta); - BoundingBox3Base inflated(coordf_t delta) const throw() { BoundingBox3Base out(*this); out.offset(delta); return out; } - PointClass center() const; + BoundingBox3Base inflated(coordf_t delta) const throw() { BoundingBox3Base out(*this); out.offset(delta); return out; } + PointType center() const; coordf_t max_size() const; - bool contains(const PointClass &point) const { - return BoundingBoxBase::contains(point) && point.z() >= this->min.z() && point.z() <= this->max.z(); + bool contains(const PointType &point) const { + return BoundingBoxBase::contains(point) && point.z() >= this->min.z() && point.z() <= this->max.z(); } - bool contains(const BoundingBox3Base& other) const { + bool contains(const BoundingBox3Base& other) const { return contains(other.min) && contains(other.max); } // Intersects without boundaries. - bool intersects(const BoundingBox3Base& other) const { + bool intersects(const BoundingBox3Base& other) const { return this->min.x() < other.max.x() && this->max.x() > other.min.x() && this->min.y() < other.max.y() && this->max.y() > other.min.y() && this->min.z() < other.max.z() && this->max.z() > other.min.z(); } }; // Will prevent warnings caused by non existing definition of template in hpp -extern template void BoundingBoxBase::scale(double factor); +extern template void BoundingBoxBase::scale(double factor); extern template void BoundingBoxBase::scale(double factor); extern template void BoundingBoxBase::scale(double factor); -extern template void BoundingBoxBase::offset(coordf_t delta); +extern template void BoundingBoxBase::offset(coordf_t delta); extern template void BoundingBoxBase::offset(coordf_t delta); -extern template void BoundingBoxBase::merge(const Point &point); +extern template void BoundingBoxBase::merge(const Point &point); extern template void BoundingBoxBase::merge(const Vec2f &point); extern template void BoundingBoxBase::merge(const Vec2d &point); -extern template void BoundingBoxBase::merge(const Points &points); +extern template void BoundingBoxBase::merge(const Points &points); extern template void BoundingBoxBase::merge(const Pointfs &points); -extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); +extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); extern template void BoundingBoxBase::merge(const BoundingBoxBase &bb); -extern template Point BoundingBoxBase::size() const; +extern template Point BoundingBoxBase::size() const; extern template Vec2f BoundingBoxBase::size() const; extern template Vec2d BoundingBoxBase::size() const; -extern template double BoundingBoxBase::radius() const; +extern template double BoundingBoxBase::radius() const; extern template double BoundingBoxBase::radius() const; -extern template Point BoundingBoxBase::center() const; +extern template Point BoundingBoxBase::center() const; extern template Vec2f BoundingBoxBase::center() const; extern template Vec2d BoundingBoxBase::center() const; extern template void BoundingBox3Base::merge(const Vec3f &point); @@ -174,7 +177,7 @@ extern template Vec3d BoundingBox3Base::center() const; extern template coordf_t BoundingBox3Base::max_size() const; extern template coordf_t BoundingBox3Base::max_size() const; -class BoundingBox : public BoundingBoxBase +class BoundingBox : public BoundingBoxBase { public: void polygon(Polygon* polygon) const; @@ -187,9 +190,9 @@ public: // to encompass the original bounding box. void align_to_grid(const coord_t cell_size); - BoundingBox() : BoundingBoxBase() {} - BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase(pmin, pmax) {} - BoundingBox(const Points &points) : BoundingBoxBase(points) {} + BoundingBox() : BoundingBoxBase() {} + BoundingBox(const Point &pmin, const Point &pmax) : BoundingBoxBase(pmin, pmax) {} + BoundingBox(const Points &points) : BoundingBoxBase(points) {} BoundingBox inflated(coordf_t delta) const throw() { BoundingBox out(*this); out.offset(delta); return out; } @@ -222,14 +225,14 @@ public: BoundingBoxf3 transformed(const Transform3d& matrix) const; }; -template -inline bool empty(const BoundingBoxBase &bb) +template +inline bool empty(const BoundingBoxBase &bb) { return ! bb.defined || bb.min.x() >= bb.max.x() || bb.min.y() >= bb.max.y(); } -template -inline bool empty(const BoundingBox3Base &bb) +template +inline bool empty(const BoundingBox3Base &bb) { return ! bb.defined || bb.min.x() >= bb.max.x() || bb.min.y() >= bb.max.y() || bb.min.z() >= bb.max.z(); } diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 61046e961..53074d3bd 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -485,6 +485,7 @@ target_link_libraries(libslic3r qhull semver TBB::tbb + TBB::tbbmalloc libslic3r_cgal ${CMAKE_DL_LIBS} PNG::PNG diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 2869d0c87..d83fe4e48 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -51,9 +51,11 @@ namespace ClipperUtils { // Clip source polygon to be used as a clipping polygon with a bouding box around the source (to be clipped) polygon. // Useful as an optimization for expensive ClipperLib operations, for example when clipping source polygons one by one // with a set of polygons covering the whole layer below. - template - inline void clip_clipper_polygon_with_subject_bbox_templ(const std::vector &src, const BoundingBox &bbox, std::vector &out) + template + inline void clip_clipper_polygon_with_subject_bbox_templ(const PointsType &src, const BoundingBox &bbox, PointsType &out) { + using PointType = typename PointsType::value_type; + out.clear(); const size_t cnt = src.size(); if (cnt < 3) @@ -108,10 +110,10 @@ namespace ClipperUtils { void clip_clipper_polygon_with_subject_bbox(const ZPoints &src, const BoundingBox &bbox, ZPoints &out) { clip_clipper_polygon_with_subject_bbox_templ(src, bbox, out); } - template - [[nodiscard]] std::vector clip_clipper_polygon_with_subject_bbox_templ(const std::vector &src, const BoundingBox &bbox) + template + [[nodiscard]] PointsType clip_clipper_polygon_with_subject_bbox_templ(const PointsType &src, const BoundingBox &bbox) { - std::vector out; + PointsType out; clip_clipper_polygon_with_subject_bbox(src, bbox, out); return out; } diff --git a/src/libslic3r/ClipperZUtils.hpp b/src/libslic3r/ClipperZUtils.hpp index 4ae78ae23..001a3f2da 100644 --- a/src/libslic3r/ClipperZUtils.hpp +++ b/src/libslic3r/ClipperZUtils.hpp @@ -40,7 +40,7 @@ inline ZPath to_zpath(const Points &path, coord_t z) // Convert multiple paths to paths with a given Z coordinate. // If Open, then duplicate the first point of each path at its end. template -inline ZPaths to_zpaths(const std::vector &paths, coord_t z) +inline ZPaths to_zpaths(const VecOfPoints &paths, coord_t z) { ZPaths out; out.reserve(paths.size()); @@ -86,16 +86,16 @@ inline Points from_zpath(const ZPoints &path) // Convert multiple paths to paths with a given Z coordinate. // If Open, then duplicate the first point of each path at its end. template -inline void from_zpaths(const ZPaths &paths, std::vector &out) +inline void from_zpaths(const ZPaths &paths, VecOfPoints &out) { out.reserve(out.size() + paths.size()); for (const ZPoints &path : paths) out.emplace_back(from_zpath(path)); } template -inline std::vector from_zpaths(const ZPaths &paths) +inline VecOfPoints from_zpaths(const ZPaths &paths) { - std::vector out; + VecOfPoints out; from_zpaths(paths, out); return out; } diff --git a/src/libslic3r/EdgeGrid.hpp b/src/libslic3r/EdgeGrid.hpp index 4be2bdd07..744a23e18 100644 --- a/src/libslic3r/EdgeGrid.hpp +++ b/src/libslic3r/EdgeGrid.hpp @@ -17,7 +17,7 @@ public: Contour() = default; Contour(const Slic3r::Point *begin, const Slic3r::Point *end, bool open) : m_begin(begin), m_end(end), m_open(open) {} Contour(const Slic3r::Point *data, size_t size, bool open) : Contour(data, data + size, open) {} - Contour(const std::vector &pts, bool open) : Contour(pts.data(), pts.size(), open) {} + Contour(const Points &pts, bool open) : Contour(pts.data(), pts.size(), open) {} const Slic3r::Point *begin() const { return m_begin; } const Slic3r::Point *end() const { return m_end; } diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index b6d12a602..42f026e0b 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -397,7 +397,7 @@ bool has_duplicate_points(const ExPolygon &expoly) size_t cnt = expoly.contour.points.size(); for (const Polygon &hole : expoly.holes) cnt += hole.points.size(); - std::vector allpts; + Points allpts; allpts.reserve(cnt); allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); for (const Polygon &hole : expoly.holes) @@ -420,7 +420,7 @@ bool has_duplicate_points(const ExPolygons &expolys) // Check globally. #if 0 // Detect duplicates by sorting with quicksort. It is quite fast, but ankerl::unordered_dense is around 1/4 faster. - std::vector allpts; + Points allpts; allpts.reserve(count_points(expolys)); for (const ExPolygon &expoly : expolys) { allpts.insert(allpts.begin(), expoly.contour.points.begin(), expoly.contour.points.end()); diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 625ea695c..1488a78a6 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -110,12 +110,14 @@ struct ExtendedPoint float curvature; }; -template -std::vector estimate_points_properties(const std::vector

&input_points, +template +std::vector estimate_points_properties(const POINTS &input_points, const AABBTreeLines::LinesDistancer &unscaled_prev_layer, float flow_width, float max_line_length = -1.0f) { + using P = typename POINTS::value_type; + using AABBScalar = typename AABBTreeLines::LinesDistancer::Scalar; if (input_points.empty()) return {}; diff --git a/src/libslic3r/GCode/SeamPlacer.cpp b/src/libslic3r/GCode/SeamPlacer.cpp index 244a103ca..3bf1edf39 100644 --- a/src/libslic3r/GCode/SeamPlacer.cpp +++ b/src/libslic3r/GCode/SeamPlacer.cpp @@ -437,7 +437,7 @@ Polygons extract_perimeter_polygons(const Layer *layer, std::vector; - } - namespace Geometry { // Generic result of an orientation predicate. diff --git a/src/libslic3r/JumpPointSearch.cpp b/src/libslic3r/JumpPointSearch.cpp index ef3dba45e..58a656fa0 100644 --- a/src/libslic3r/JumpPointSearch.cpp +++ b/src/libslic3r/JumpPointSearch.cpp @@ -19,6 +19,8 @@ #include #include +#include + //#define DEBUG_FILES #ifdef DEBUG_FILES #include "libslic3r/SVG.hpp" @@ -267,7 +269,7 @@ Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1) using QNode = astar::QNode>; std::unordered_map astar_cache{}; - std::vector out_path; + std::vector> out_path; std::vector out_nodes; if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) { @@ -306,7 +308,7 @@ Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1) svg.draw(scaled_point(start), "green", scale_(0.4)); #endif - std::vector tmp_path; + std::vector> tmp_path; tmp_path.reserve(out_path.size()); // Some path found, reverse and remove points that do not change direction std::reverse(out_path.begin(), out_path.end()); diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index f18720bd6..bb4d62cc0 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -103,9 +103,9 @@ bool MultiPoint::remove_duplicate_points() return false; } -std::vector MultiPoint::_douglas_peucker(const std::vector& pts, const double tolerance) +Points MultiPoint::_douglas_peucker(const Points &pts, const double tolerance) { - std::vector result_pts; + Points result_pts; double tolerance_sq = tolerance * tolerance; if (! pts.empty()) { const Point *anchor = &pts.front(); diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index 778b30c57..4cf4b5e14 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -110,7 +110,7 @@ public: }; extern BoundingBox get_extents(const MultiPoint &mp); -extern BoundingBox get_extents_rotated(const std::vector &points, double angle); +extern BoundingBox get_extents_rotated(const Points &points, double angle); extern BoundingBox get_extents_rotated(const MultiPoint &mp, double angle); inline double length(const Points &pts) { diff --git a/src/libslic3r/Point.cpp b/src/libslic3r/Point.cpp index 09afcc399..457bb44ce 100644 --- a/src/libslic3r/Point.cpp +++ b/src/libslic3r/Point.cpp @@ -57,7 +57,7 @@ void Point::rotate(double angle, const Point ¢er) (*this)(1) = (coord_t)round( (double)center(1) + c * dy + s * dx ); } -bool has_duplicate_points(std::vector &&pts) +bool has_duplicate_points(Points &&pts) { std::sort(pts.begin(), pts.end()); for (size_t i = 1; i < pts.size(); ++ i) @@ -97,15 +97,15 @@ template BoundingBox get_extents(const Points &pts); // if IncludeBoundary, then a bounding box is defined even for a single point. // otherwise a bounding box is only defined if it has a positive area. template -BoundingBox get_extents(const std::vector &pts) +BoundingBox get_extents(const VecOfPoints &pts) { BoundingBox bbox; for (const Points &p : pts) bbox.merge(get_extents(p)); return bbox; } -template BoundingBox get_extents(const std::vector &pts); -template BoundingBox get_extents(const std::vector &pts); +template BoundingBox get_extents(const VecOfPoints &pts); +template BoundingBox get_extents(const VecOfPoints &pts); BoundingBoxf get_extents(const std::vector &pts) { diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index d53352f28..72c739395 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -9,6 +9,9 @@ #include #include +#include + + #include #include "LocalesUtils.hpp" @@ -49,7 +52,7 @@ using Vec2d = Eigen::Matrix; using Vec3d = Eigen::Matrix; using Vec4d = Eigen::Matrix; -using Points = std::vector; +using Points = std::vector>; using PointPtrs = std::vector; using PointConstPtrs = std::vector; using Points3 = std::vector; @@ -57,6 +60,8 @@ using Pointfs = std::vector; using Vec2ds = std::vector; using Pointf3s = std::vector; +using VecOfPoints = std::vector>; + using Matrix2f = Eigen::Matrix; using Matrix2d = Eigen::Matrix; using Matrix3f = Eigen::Matrix; @@ -247,9 +252,9 @@ extern template BoundingBox get_extents(const Points &pts); // if IncludeBoundary, then a bounding box is defined even for a single point. // otherwise a bounding box is only defined if it has a positive area. template -BoundingBox get_extents(const std::vector &pts); -extern template BoundingBox get_extents(const std::vector &pts); -extern template BoundingBox get_extents(const std::vector &pts); +BoundingBox get_extents(const VecOfPoints &pts); +extern template BoundingBox get_extents(const VecOfPoints &pts); +extern template BoundingBox get_extents(const VecOfPoints &pts); BoundingBoxf get_extents(const std::vector &pts); @@ -263,16 +268,16 @@ inline std::pair nearest_point(const Points &points, const Point &p // Test for duplicate points in a vector of points. // The points are copied, sorted and checked for duplicates globally. -bool has_duplicate_points(std::vector &&pts); -inline bool has_duplicate_points(const std::vector &pts) +bool has_duplicate_points(Points &&pts); +inline bool has_duplicate_points(const Points &pts) { - std::vector cpy = pts; + Points cpy = pts; return has_duplicate_points(std::move(cpy)); } // Test for duplicate points in a vector of points. // Only successive points are checked for equality. -inline bool has_duplicate_successive_points(const std::vector &pts) +inline bool has_duplicate_successive_points(const Points &pts) { for (size_t i = 1; i < pts.size(); ++ i) if (pts[i - 1] == pts[i]) @@ -282,7 +287,7 @@ inline bool has_duplicate_successive_points(const std::vector &pts) // Test for duplicate points in a vector of points. // Only successive points are checked for equality. Additionally, first and last points are compared for equality. -inline bool has_duplicate_successive_points_closed(const std::vector &pts) +inline bool has_duplicate_successive_points_closed(const Points &pts) { return has_duplicate_successive_points(pts) || (pts.size() >= 2 && pts.front() == pts.back()); } diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 3be36984c..299e22adc 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -404,7 +404,7 @@ bool has_duplicate_points(const Polygons &polys) // Check globally. #if 0 // Detect duplicates by sorting with quicksort. It is quite fast, but ankerl::unordered_dense is around 1/4 faster. - std::vector allpts; + Points allpts; allpts.reserve(count_points(polys)); for (const Polygon &poly : polys) allpts.insert(allpts.end(), poly.points.begin(), poly.points.end()); diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 3c4bb0e2a..24a2d69f3 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -5,6 +5,7 @@ #include #include #include "Line.hpp" +#include "Point.hpp" #include "MultiPoint.hpp" #include "Polyline.hpp" @@ -241,7 +242,7 @@ inline Polylines to_polylines(Polygons &&polys) return polylines; } -inline Polygons to_polygons(const std::vector &paths) +inline Polygons to_polygons(const VecOfPoints &paths) { Polygons out; out.reserve(paths.size()); @@ -250,7 +251,7 @@ inline Polygons to_polygons(const std::vector &paths) return out; } -inline Polygons to_polygons(std::vector &&paths) +inline Polygons to_polygons(VecOfPoints &&paths) { Polygons out; out.reserve(paths.size()); diff --git a/src/libslic3r/PolygonTrimmer.hpp b/src/libslic3r/PolygonTrimmer.hpp index eddffbc7f..93e94e303 100644 --- a/src/libslic3r/PolygonTrimmer.hpp +++ b/src/libslic3r/PolygonTrimmer.hpp @@ -17,7 +17,7 @@ namespace EdgeGrid { struct TrimmedLoop { - std::vector points; + Points points; // Number of points per segment. Empty if the loop is std::vector segments; diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 1b23388e1..9ed5f2137 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -158,9 +158,10 @@ inline void polylines_append(Polylines &dst, Polylines &&src) // src_first: the merge point is at src.begin() or src.end()? // The orientation of the resulting polyline is unknown, the output polyline may start // either with src piece or dst piece. -template -inline void polylines_merge(std::vector &dst, bool dst_first, std::vector &&src, bool src_first) +template +inline void polylines_merge(PointsType &dst, bool dst_first, PointsType &&src, bool src_first) { + using PointType = typename PointsType::value_type; if (dst_first) { if (src_first) std::reverse(dst.begin(), dst.end()); diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b68d04b2b..49d17e5c0 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1132,9 +1132,9 @@ Polygons Print::first_layer_islands() const return islands; } -std::vector Print::first_layer_wipe_tower_corners() const +Points Print::first_layer_wipe_tower_corners() const { - std::vector pts_scaled; + Points pts_scaled; if (has_wipe_tower() && ! m_wipe_tower_data.tool_changes.empty()) { double width = m_config.wipe_tower_width + 2*m_wipe_tower_data.brim_width; diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index d222fbfe2..30f6bb41a 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -626,7 +626,7 @@ private: // Islands of objects and their supports extruded at the 1st layer. Polygons first_layer_islands() const; // Return 4 wipe tower corners in the world coordinates (shifted and rotated), including the wipe tower brim. - std::vector first_layer_wipe_tower_corners() const; + Points first_layer_wipe_tower_corners() const; // Returns true if any of the print_objects has print_object_step valid. // That means data shared by all print objects of the print_objects span may still use the shared data. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0c3bf87b9..bc0370aee 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1990,8 +1990,8 @@ void PrintObject::bridge_over_infill() // reconstruct polygon from polygon sections struct TracedPoly { - std::vector lows; - std::vector highs; + Points lows; + Points highs; }; std::vector current_traced_polys; diff --git a/src/libslic3r/SLA/ConcaveHull.cpp b/src/libslic3r/SLA/ConcaveHull.cpp index 08a2ff676..f657fce74 100644 --- a/src/libslic3r/SLA/ConcaveHull.cpp +++ b/src/libslic3r/SLA/ConcaveHull.cpp @@ -43,7 +43,8 @@ Point ConcaveHull::centroid(const Points &pp) Points ConcaveHull::calculate_centroids() const { // We get the centroids of all the islands in the 2D slice - Points centroids = reserve_vector(m_polys.size()); + Points centroids; + centroids.reserve(m_polys.size()); std::transform(m_polys.begin(), m_polys.end(), std::back_inserter(centroids), [](const Polygon &poly) { return centroid(poly); }); diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp index c84349217..93259fee8 100644 --- a/src/libslic3r/ShortestPath.hpp +++ b/src/libslic3r/ShortestPath.hpp @@ -8,10 +8,15 @@ #include #include -namespace ClipperLib { class PolyNode; } +#include namespace Slic3r { + namespace ClipperLib { + class PolyNode; + using PolyNodes = std::vector>; + } + class ExPolygon; using ExPolygons = std::vector; @@ -29,7 +34,7 @@ void chain_and_reorder_extrusion_paths(std::vect Polylines chain_polylines(Polylines &&src, const Point *start_near = nullptr); inline Polylines chain_polylines(const Polylines& src, const Point* start_near = nullptr) { Polylines tmp(src); return chain_polylines(std::move(tmp), start_near); } -std::vector chain_clipper_polynodes(const Points &points, const std::vector &items); +ClipperLib::PolyNodes chain_clipper_polynodes(const Points &points, const ClipperLib::PolyNodes &items); // Chain instances of print objects by an approximate shortest path. // Returns pairs of PrintObject idx and instance of that PrintObject. diff --git a/src/libslic3r/libslic3r.h b/src/libslic3r/libslic3r.h index 856208c9d..83089fefe 100644 --- a/src/libslic3r/libslic3r.h +++ b/src/libslic3r/libslic3r.h @@ -106,8 +106,8 @@ enum Axis { NUM_AXES_WITH_UNKNOWN, }; -template -inline void append(std::vector& dest, const std::vector& src) +template +inline void append(std::vector &dest, const std::vector &src) { if (dest.empty()) dest = src; // copy @@ -115,8 +115,8 @@ inline void append(std::vector& dest, const std::vector& src) dest.insert(dest.end(), src.begin(), src.end()); } -template -inline void append(std::vector& dest, std::vector&& src) +template +inline void append(std::vector &dest, std::vector &&src) { if (dest.empty()) dest = std::move(src); diff --git a/tests/libnest2d/CMakeLists.txt b/tests/libnest2d/CMakeLists.txt index ea4f4255e..8dabc688d 100644 --- a/tests/libnest2d/CMakeLists.txt +++ b/tests/libnest2d/CMakeLists.txt @@ -2,7 +2,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp printer_parts.cpp printer_parts.hpp) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. -target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libnest2d ) +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libnest2d ) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") # catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ") diff --git a/tests/libslic3r/test_geometry.cpp b/tests/libslic3r/test_geometry.cpp index 239edd4f7..16a27665e 100644 --- a/tests/libslic3r/test_geometry.cpp +++ b/tests/libslic3r/test_geometry.cpp @@ -83,7 +83,7 @@ TEST_CASE("Line::perpendicular_to", "[Geometry]") { TEST_CASE("Polygon::contains works properly", "[Geometry]"){ // this test was failing on Windows (GH #1950) - Slic3r::Polygon polygon(std::vector({ + Slic3r::Polygon polygon(Points({ Point(207802834,-57084522), Point(196528149,-37556190), Point(173626821,-25420928), @@ -145,7 +145,7 @@ SCENARIO("polygon_is_convex works") { TEST_CASE("Creating a polyline generates the obvious lines", "[Geometry]"){ Slic3r::Polyline polyline; - polyline.points = std::vector({Point(0, 0), Point(10, 0), Point(20, 0)}); + polyline.points = Points({Point(0, 0), Point(10, 0), Point(20, 0)}); REQUIRE(polyline.lines().at(0).a == Point(0,0)); REQUIRE(polyline.lines().at(0).b == Point(10,0)); REQUIRE(polyline.lines().at(1).a == Point(10,0)); @@ -153,7 +153,7 @@ TEST_CASE("Creating a polyline generates the obvious lines", "[Geometry]"){ } TEST_CASE("Splitting a Polygon generates a polyline correctly", "[Geometry]"){ - Slic3r::Polygon polygon(std::vector({Point(0, 0), Point(10, 0), Point(5, 5)})); + Slic3r::Polygon polygon(Points({Point(0, 0), Point(10, 0), Point(5, 5)})); Slic3r::Polyline split = polygon.split_at_index(1); REQUIRE(split.points[0]==Point(10,0)); REQUIRE(split.points[1]==Point(5,5)); @@ -164,7 +164,7 @@ TEST_CASE("Splitting a Polygon generates a polyline correctly", "[Geometry]"){ SCENARIO("BoundingBox", "[Geometry]") { WHEN("Bounding boxes are scaled") { - BoundingBox bb(std::vector({Point(0, 1), Point(10, 2), Point(20, 2)})); + BoundingBox bb(Points({Point(0, 1), Point(10, 2), Point(20, 2)})); bb.scale(2); REQUIRE(bb.min == Point(0,2)); REQUIRE(bb.max == Point(40,4)); @@ -193,7 +193,7 @@ SCENARIO("BoundingBox", "[Geometry]") { TEST_CASE("Offseting a line generates a polygon correctly", "[Geometry]"){ Slic3r::Polyline tmp = { Point(10,10), Point(20,10) }; Slic3r::Polygon area = offset(tmp,5).at(0); - REQUIRE(area.area() == Slic3r::Polygon(std::vector({Point(10,5),Point(20,5),Point(20,15),Point(10,15)})).area()); + REQUIRE(area.area() == Slic3r::Polygon(Points({Point(10,5),Point(20,5),Point(20,15),Point(10,15)})).area()); } SCENARIO("Circle Fit, TaubinFit with Newton's method", "[Geometry]") { @@ -308,7 +308,7 @@ TEST_CASE("smallest_enclosing_circle_welzl", "[Geometry]") { SCENARIO("Path chaining", "[Geometry]") { GIVEN("A path") { - std::vector points = { Point(26,26),Point(52,26),Point(0,26),Point(26,52),Point(26,0),Point(0,52),Point(52,52),Point(52,0) }; + Points points = { Point(26,26),Point(52,26),Point(0,26),Point(26,52),Point(26,0),Point(0,52),Point(52,52),Point(52,0) }; THEN("Chained with no diagonals (thus 26 units long)") { std::vector indices = chain_points(points); for (Points::size_type i = 0; i + 1 < indices.size(); ++ i) { @@ -431,7 +431,7 @@ SCENARIO("Calculating angles", "[Geometry]") SCENARIO("Polygon convex/concave detection", "[Geometry]"){ static constexpr const double angle_threshold = M_PI / 3.; GIVEN(("A Square with dimension 100")){ - auto square = Slic3r::Polygon /*new_scale*/(std::vector({ + auto square = Slic3r::Polygon /*new_scale*/(Points({ Point(100,100), Point(200,100), Point(200,200), @@ -447,7 +447,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ } } GIVEN("A Square with an extra colinearvertex"){ - auto square = Slic3r::Polygon /*new_scale*/(std::vector({ + auto square = Slic3r::Polygon /*new_scale*/(Points({ Point(150,100), Point(200,100), Point(200,200), @@ -459,7 +459,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ } } GIVEN("A Square with an extra collinear vertex in different order"){ - auto square = Slic3r::Polygon /*new_scale*/(std::vector({ + auto square = Slic3r::Polygon /*new_scale*/(Points({ Point(200,200), Point(100,200), Point(100,100), @@ -472,7 +472,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ } GIVEN("A triangle"){ - auto triangle = Slic3r::Polygon(std::vector({ + auto triangle = Slic3r::Polygon(Points({ Point(16000170,26257364), Point(714223,461012), Point(31286371,461008) @@ -484,7 +484,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ } GIVEN("A triangle with an extra collinear point"){ - auto triangle = Slic3r::Polygon(std::vector({ + auto triangle = Slic3r::Polygon(Points({ Point(16000170,26257364), Point(714223,461012), Point(20000000,461012), @@ -498,7 +498,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ GIVEN("A polygon with concave vertices with angles of specifically 4/3pi"){ // Two concave vertices of this polygon have angle = PI*4/3, so this test fails // if epsilon is not used. - auto polygon = Slic3r::Polygon(std::vector({ + auto polygon = Slic3r::Polygon(Points({ Point(60246458,14802768),Point(64477191,12360001), Point(63727343,11060995),Point(64086449,10853608), Point(66393722,14850069),Point(66034704,15057334), @@ -516,7 +516,7 @@ SCENARIO("Polygon convex/concave detection", "[Geometry]"){ } TEST_CASE("Triangle Simplification does not result in less than 3 points", "[Geometry]"){ - auto triangle = Slic3r::Polygon(std::vector({ + auto triangle = Slic3r::Polygon(Points({ Point(16000170,26257364), Point(714223,461012), Point(31286371,461008) })); REQUIRE(triangle.simplify(250000).at(0).points.size() == 3); diff --git a/tests/sla_print/CMakeLists.txt b/tests/sla_print/CMakeLists.txt index 2a800cc50..3a5d96c7a 100644 --- a/tests/sla_print/CMakeLists.txt +++ b/tests/sla_print/CMakeLists.txt @@ -8,7 +8,7 @@ add_executable(${_TEST_NAME}_tests ${_TEST_NAME}_tests_main.cpp sla_archive_readwrite_tests.cpp) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. -target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libslic3r) +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libslic3r) set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests") if (WIN32) diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index 892e7a8a7..7c83fb8d7 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -6,7 +6,7 @@ add_executable(${_TEST_NAME}_tests ) # mold linker for successful linking needs also to link TBB library and link it before libslic3r. -target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb libslic3r_gui libslic3r) +target_link_libraries(${_TEST_NAME}_tests test_common TBB::tbb TBB::tbbmalloc libslic3r_gui libslic3r) if (MSVC) target_link_libraries(${_TEST_NAME}_tests Setupapi.lib) endif () From bc2e6819323ce114a92e63381980517543838ffc Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Thu, 20 Apr 2023 14:00:19 +0200 Subject: [PATCH 041/115] SPE-1669 and follow-up of c1e145b86c6da0fedf257f39c79934d2d80b46d4 - Fixed crash introduced with the previous commit and extend the new functionality to multi-objects selections --- src/slic3r/GUI/Plater.cpp | 22 +++++++++++++++++----- src/slic3r/GUI/Plater.hpp | 2 +- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 5808b196d..6adabb7d6 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6057,7 +6057,7 @@ void Plater::remove_selected() p->view3D->delete_selected(); } -void Plater::increase_instances(size_t num, int obj_idx/* = -1*/) +void Plater::increase_instances(size_t num, int obj_idx, std::optional selection_map) { if (! can_increase_instances()) { return; } @@ -6067,14 +6067,26 @@ void Plater::increase_instances(size_t num, int obj_idx/* = -1*/) obj_idx = p->get_selected_object_idx(); if (obj_idx < 0) { - if (const auto obj_idxs = get_selection().get_object_idxs(); !obj_idxs.empty()) - for (const size_t obj_id : obj_idxs) - increase_instances(1, int(obj_id)); + if (const auto obj_idxs = get_selection().get_object_idxs(); !obj_idxs.empty()) { + // we need a copy made here because the selection changes at every call of increase_instances() + const Selection::ObjectIdxsToInstanceIdxsMap content = selection_map.has_value() ? *selection_map : p->get_selection().get_content(); + for (const size_t obj_id : obj_idxs) { + increase_instances(1, int(obj_id), content); + } + } return; } ModelObject* model_object = p->model.objects[obj_idx]; - const int inst_idx = p->get_selected_instance_idx(); + int inst_idx = -1; + if (selection_map.has_value()) { + auto obj_it = selection_map->find(obj_idx); + if (obj_it != selection_map->end() && obj_it->second.size() == 1) + inst_idx = *obj_it->second.begin(); + } + else + inst_idx = p->get_selected_instance_idx(); + ModelInstance* model_instance = (inst_idx >= 0) ? model_object->instances[inst_idx] : model_object->instances.back(); bool was_one_instance = model_object->instances.size()==1; diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp index fa2ec6508..a76ef6f1c 100644 --- a/src/slic3r/GUI/Plater.hpp +++ b/src/slic3r/GUI/Plater.hpp @@ -250,7 +250,7 @@ public: void reset_with_confirm(); bool delete_object_from_model(size_t obj_idx); void remove_selected(); - void increase_instances(size_t num = 1, int obj_idx = -1); + void increase_instances(size_t num = 1, int obj_idx = -1, std::optional selection_map = std::nullopt); void decrease_instances(size_t num = 1, int obj_idx = -1); void set_number_of_copies(); void fill_bed_with_instances(); From b67ad6434d1e5b1b54a13e83ceaef4141f9ad35f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 20 Apr 2023 14:30:52 +0200 Subject: [PATCH 042/115] Follow-up to 9cde96993e9f996b44f417570ba05455472efa08 use tbb::scallable_allocator for Polygons and ExPolygon::holes to better scale on multiple threads --- src/clipper/clipper.hpp | 2 +- src/libslic3r/Arachne/WallToolPaths.cpp | 2 +- src/libslic3r/ClipperUtils.hpp | 14 +++++++------- src/libslic3r/JumpPointSearch.cpp | 4 ++-- src/libslic3r/Point.hpp | 6 ++++-- src/libslic3r/Polygon.hpp | 6 +++--- src/libslic3r/SLA/Pad.hpp | 4 +++- src/libslic3r/SLA/SupportTree.hpp | 4 +--- src/libslic3r/ShortestPath.hpp | 4 +--- src/slic3r/GUI/GLModel.hpp | 2 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 3 ++- tests/libslic3r/test_clipper_utils.cpp | 4 ++-- 12 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index a2a6712b2..1d6361653 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -281,7 +281,7 @@ enum EdgeSide { esLeft = 1, esRight = 2}; OutPt *Prev; }; - using OutPts = std::vector>; + using OutPts = std::vector>; struct OutRec; struct Join { diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index dd2cda271..1c69fa9ac 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -341,7 +341,7 @@ void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool re } } else { // For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes - std::vector small_holes; + Polygons small_holes; for (auto it = thiss.begin(); it < new_end;) { if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) { if (area >= 0) { diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index 5f495788b..aaa06107d 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -133,21 +133,21 @@ namespace ClipperUtils { const std::vector &m_paths; }; - template + template class MultiPointsProvider { public: - MultiPointsProvider(const std::vector &multipoints) : m_multipoints(multipoints) {} + MultiPointsProvider(const MultiPointsType &multipoints) : m_multipoints(multipoints) {} struct iterator : public PathsProviderIteratorBase { public: - explicit iterator(typename std::vector::const_iterator it) : m_it(it) {} + explicit iterator(typename MultiPointsType::const_iterator it) : m_it(it) {} const Points& operator*() const { return m_it->points; } bool operator==(const iterator &rhs) const { return m_it == rhs.m_it; } bool operator!=(const iterator &rhs) const { return !(*this == rhs); } const Points& operator++(int) { return (m_it ++)->points; } iterator& operator++() { ++ m_it; return *this; } private: - typename std::vector::const_iterator m_it; + typename MultiPointsType::const_iterator m_it; }; iterator cbegin() const { return iterator(m_multipoints.begin()); } @@ -157,11 +157,11 @@ namespace ClipperUtils { size_t size() const { return m_multipoints.size(); } private: - const std::vector &m_multipoints; + const MultiPointsType &m_multipoints; }; - using PolygonsProvider = MultiPointsProvider; - using PolylinesProvider = MultiPointsProvider; + using PolygonsProvider = MultiPointsProvider; + using PolylinesProvider = MultiPointsProvider; struct ExPolygonProvider { ExPolygonProvider(const ExPolygon &expoly) : m_expoly(expoly) {} diff --git a/src/libslic3r/JumpPointSearch.cpp b/src/libslic3r/JumpPointSearch.cpp index 58a656fa0..722415632 100644 --- a/src/libslic3r/JumpPointSearch.cpp +++ b/src/libslic3r/JumpPointSearch.cpp @@ -269,7 +269,7 @@ Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1) using QNode = astar::QNode>; std::unordered_map astar_cache{}; - std::vector> out_path; + std::vector> out_path; std::vector out_nodes; if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) { @@ -308,7 +308,7 @@ Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1) svg.draw(scaled_point(start), "green", scale_(0.4)); #endif - std::vector> tmp_path; + std::vector> tmp_path; tmp_path.reserve(out_path.size()); // Some path found, reverse and remove points that do not change direction std::reverse(out_path.begin(), out_path.end()); diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index 72c739395..b482129cf 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -52,7 +52,9 @@ using Vec2d = Eigen::Matrix; using Vec3d = Eigen::Matrix; using Vec4d = Eigen::Matrix; -using Points = std::vector>; +template +using PointsAllocator = tbb::scalable_allocator; +using Points = std::vector>; using PointPtrs = std::vector; using PointConstPtrs = std::vector; using Points3 = std::vector; @@ -60,7 +62,7 @@ using Pointfs = std::vector; using Vec2ds = std::vector; using Pointf3s = std::vector; -using VecOfPoints = std::vector>; +using VecOfPoints = std::vector>; using Matrix2f = Eigen::Matrix; using Matrix2d = Eigen::Matrix; diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index 24a2d69f3..bf4a087b0 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -12,9 +12,9 @@ namespace Slic3r { class Polygon; -using Polygons = std::vector; -using PolygonPtrs = std::vector; -using ConstPolygonPtrs = std::vector; +using Polygons = std::vector>; +using PolygonPtrs = std::vector>; +using ConstPolygonPtrs = std::vector>; // Returns true if inside. Returns border_result if on boundary. bool contains(const Polygon& polygon, const Point& p, bool border_result = true); diff --git a/src/libslic3r/SLA/Pad.hpp b/src/libslic3r/SLA/Pad.hpp index 0b6149557..da09343c4 100644 --- a/src/libslic3r/SLA/Pad.hpp +++ b/src/libslic3r/SLA/Pad.hpp @@ -6,6 +6,8 @@ #include #include +#include + struct indexed_triangle_set; namespace Slic3r { @@ -13,7 +15,7 @@ namespace Slic3r { class ExPolygon; class Polygon; using ExPolygons = std::vector; -using Polygons = std::vector; +using Polygons = std::vector>; namespace sla { diff --git a/src/libslic3r/SLA/SupportTree.hpp b/src/libslic3r/SLA/SupportTree.hpp index 53fb16f6e..83814d8c5 100644 --- a/src/libslic3r/SLA/SupportTree.hpp +++ b/src/libslic3r/SLA/SupportTree.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -14,9 +15,6 @@ namespace Slic3r { -using Polygons = std::vector; -using ExPolygons = std::vector; - namespace sla { struct SupportTreeConfig diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp index 93259fee8..1781c5188 100644 --- a/src/libslic3r/ShortestPath.hpp +++ b/src/libslic3r/ShortestPath.hpp @@ -8,13 +8,11 @@ #include #include -#include - namespace Slic3r { namespace ClipperLib { class PolyNode; - using PolyNodes = std::vector>; + using PolyNodes = std::vector>; } class ExPolygon; diff --git a/src/slic3r/GUI/GLModel.hpp b/src/slic3r/GUI/GLModel.hpp index ef4ab6d47..fbf6ed533 100644 --- a/src/slic3r/GUI/GLModel.hpp +++ b/src/slic3r/GUI/GLModel.hpp @@ -14,7 +14,7 @@ namespace Slic3r { class TriangleMesh; class Polygon; -using Polygons = std::vector; +using Polygons = std::vector>; class BuildVolume; namespace GUI { diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 8115136a5..870957018 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -183,7 +183,8 @@ static void update_arrangepoly_slaprint(arrangement::ArrangePolygon &ret, trafo_instance = trafo_instance * po.trafo().cast().inverse(); - auto polys = reserve_vector(3); + Polygons polys; + polys.reserve(3); auto zlvl = -po.get_elevation(); if (omesh) { diff --git a/tests/libslic3r/test_clipper_utils.cpp b/tests/libslic3r/test_clipper_utils.cpp index 775796ba7..1f3bc0fdc 100644 --- a/tests/libslic3r/test_clipper_utils.cpp +++ b/tests/libslic3r/test_clipper_utils.cpp @@ -308,8 +308,8 @@ SCENARIO("Various Clipper operations - t/clipper.t", "[ClipperUtils]") { } } -template -double polytree_area(const Tree &tree, std::vector

*out) +template +double polytree_area(const Tree &tree, std::vector *out) { traverse_pt(tree, out); From ed65cdd955eca9154ca711bce2c48985af11f8e4 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 20 Apr 2023 18:31:34 +0200 Subject: [PATCH 043/115] Fix compilation of Perl bindings. --- xs/src/xsinit.h | 1 + 1 file changed, 1 insertion(+) diff --git a/xs/src/xsinit.h b/xs/src/xsinit.h index 19e25c54d..dcf56a6d4 100644 --- a/xs/src/xsinit.h +++ b/xs/src/xsinit.h @@ -77,6 +77,7 @@ #undef accept #undef wait #undef abort + #undef pause // Breaks compilation with Eigen matrices embedded into Slic3r::Point. #undef malloc From b3b44681a9461550ee148206f329d55c0c9459eb Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 21 Apr 2023 08:31:16 +0200 Subject: [PATCH 044/115] Follow-up after 9cde96993e9f996b44f417570ba05455472efa08 b67ad6434d1e5b1b54a13e83ceaef4141f9ad35f Fixed compilation on GCC and CLang --- src/libslic3r/BoundingBox.cpp | 4 ++-- src/libslic3r/Polyline.hpp | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/BoundingBox.cpp b/src/libslic3r/BoundingBox.cpp index 10543fc95..7e88eb92d 100644 --- a/src/libslic3r/BoundingBox.cpp +++ b/src/libslic3r/BoundingBox.cpp @@ -160,14 +160,14 @@ template Vec3d BoundingBox3Base::size() const; template double BoundingBoxBase::radius() const { assert(this->defined); - return 0.5 * (this->max - this->min).cast().norm(); + return 0.5 * (this->max - this->min).template cast().norm(); } template double BoundingBoxBase::radius() const; template double BoundingBoxBase::radius() const; template double BoundingBox3Base::radius() const { - return 0.5 * (this->max - this->min).cast().norm(); + return 0.5 * (this->max - this->min).template cast().norm(); } template double BoundingBox3Base::radius() const; diff --git a/src/libslic3r/Polyline.hpp b/src/libslic3r/Polyline.hpp index 9ed5f2137..8766c6d86 100644 --- a/src/libslic3r/Polyline.hpp +++ b/src/libslic3r/Polyline.hpp @@ -161,7 +161,6 @@ inline void polylines_append(Polylines &dst, Polylines &&src) template inline void polylines_merge(PointsType &dst, bool dst_first, PointsType &&src, bool src_first) { - using PointType = typename PointsType::value_type; if (dst_first) { if (src_first) std::reverse(dst.begin(), dst.end()); From 69b69cb9a2a64e8c8b1153a774056808e5463fa6 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 21 Apr 2023 09:09:04 +0200 Subject: [PATCH 045/115] Follow-up of 85dd2e486ab22e9d822036749efd75f0e675ccc9 - Fixed Zoom to mouse cursor for orthograpic camera --- src/slic3r/GUI/GLCanvas3D.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 72aaa5944..23079b63c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2943,8 +2943,8 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt) const double delta = direction_factor * (double)evt.GetWheelRotation() / (double)evt.GetWheelDelta(); if (wxGetKeyState(WXK_SHIFT)) { const auto cnv_size = get_canvas_size(); - const auto screen_center_3d_pos = _mouse_to_3d({ cnv_size.get_width() * 0.5, cnv_size.get_height() * 0.5 }); - const auto mouse_3d_pos = _mouse_to_3d({ evt.GetX(), evt.GetY() }); + const Vec3d screen_center_3d_pos = _mouse_to_3d({ cnv_size.get_width() * 0.5, cnv_size.get_height() * 0.5 }); + const Vec3d mouse_3d_pos = _mouse_to_3d({ evt.GetX(), evt.GetY() }); const Vec3d displacement = mouse_3d_pos - screen_center_3d_pos; wxGetApp().plater()->get_camera().translate_world(displacement); const double origin_zoom = wxGetApp().plater()->get_camera().get_zoom(); @@ -6267,7 +6267,8 @@ Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos) { - return mouse_ray(mouse_pos).intersect_plane(0.0); + const Linef3 ray = mouse_ray(mouse_pos); + return (std::abs(ray.unit_vector().z() < EPSILON)) ? ray.a : ray.intersect_plane(0.0); } void GLCanvas3D::_start_timer() From b04e3bc25e35e1efccb8c85d90d0475dcaa9e102 Mon Sep 17 00:00:00 2001 From: Pavel Mikus Date: Wed, 29 Mar 2023 17:14:03 +0200 Subject: [PATCH 046/115] Initial implementation, requieres both dynamic speed and avoid curled overhangs options to be enabled Also implements new, probably far better estimation of curled height of filament --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 40 ++++++++++++++----- src/libslic3r/SupportSpotsGenerator.cpp | 45 ++++++++++++++++++---- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 625ea695c..98febf609 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -246,6 +246,8 @@ class ExtrusionQualityEstimator { std::unordered_map> prev_layer_boundaries; std::unordered_map> next_layer_boundaries; + std::unordered_map> prev_curled_extrusions; + std::unordered_map> next_curled_extrusions; const PrintObject *current_object; public: @@ -253,18 +255,27 @@ public: void prepare_for_new_layer(const Layer *layer) { - if (layer == nullptr) return; - const PrintObject *object = layer->object(); - prev_layer_boundaries[object] = next_layer_boundaries[object]; - next_layer_boundaries[object] = AABBTreeLines::LinesDistancer{to_unscaled_linesf(layer->lslices)}; + if (layer == nullptr) + return; + const PrintObject *object = layer->object(); + prev_layer_boundaries[object] = next_layer_boundaries[object]; + next_layer_boundaries[object] = AABBTreeLines::LinesDistancer{to_unscaled_linesf(layer->lslices)}; + prev_curled_extrusions[object] = next_curled_extrusions[object]; + Linesf curled_lines; + curled_lines.reserve(layer->malformed_lines.size()); + for (const Line &l : layer->malformed_lines) { + curled_lines.push_back(Linef(unscaled(l.a), unscaled(l.b))); + } + next_curled_extrusions[object] = AABBTreeLines::LinesDistancer{curled_lines}; } - std::vector estimate_extrusion_quality(const ExtrusionPath &path, - const std::vector> overhangs_w_speeds, - const std::vector> overhangs_w_fan_speeds, - size_t extruder_id, - float ext_perimeter_speed, - float original_speed) + std::vector estimate_speed_from_extrusion_quality( + const ExtrusionPath &path, + const std::vector> overhangs_w_speeds, + const std::vector> overhangs_w_fan_speeds, + size_t extruder_id, + float ext_perimeter_speed, + float original_speed) { float speed_base = ext_perimeter_speed > 0 ? ext_perimeter_speed : original_speed; std::map speed_sections; @@ -285,6 +296,15 @@ public: std::vector extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width); + + for (ExtendedPoint& ep : extended_points) { + // We are going to enforce slowdown by increasing the point distance. The overhang speed is based on signed distance from + // the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width and more means full overhang, thus full slowdown. + // However, for curling, we take unsinged distance from the curled lines and artifically modifiy the distance + float distance_from_curled = prev_curled_extrusions[current_object].distance_from_lines(ep.position); + ep.distance = std::max(ep.distance, path.width - distance_from_curled); + } + std::vector processed_points; processed_points.reserve(extended_points.size()); diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 5062fe18a..bef3c0fc1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -38,7 +39,7 @@ #include "Geometry/ConvexHull.hpp" // #define DETAILED_DEBUG_LOGS -// #define DEBUG_FILES +#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -208,16 +209,44 @@ std::vector to_short_lines(const ExtrusionEntity *e, float length float estimate_curled_up_height( const ExtendedPoint &point, float layer_height, float flow_width, float prev_line_curled_height, Params params) { - float curled_up_height = 0.0f; + float curled_up_height = 0; if (fabs(point.distance) < 1.5 * flow_width) { - curled_up_height = 0.85 * prev_line_curled_height; + curled_up_height = 0.9 * prev_line_curled_height; } - if (point.distance > params.malformation_distance_factors.first * flow_width && - point.distance < params.malformation_distance_factors.second * flow_width && point.curvature > -0.1f) { - float dist_factor = std::max(point.distance - params.malformation_distance_factors.first * flow_width, 0.01f) / - ((params.malformation_distance_factors.second - params.malformation_distance_factors.first) * flow_width); - curled_up_height = layer_height * sqrt(sqrt(dist_factor)) * std::clamp(3.0f * point.curvature, 1.0f, 3.0f); + if (point.distance > params.malformation_distance_factors.first * flow_width && + point.distance < params.malformation_distance_factors.second * flow_width) { + // imagine the extrusion profile. The part that has been glued (melted) with the previous layer will be called anchored section + // and the rest will be called curling section + float anchored_section = flow_width - point.distance; + float curling_section = point.distance; + + // after extruding, the curling (floating) part of the extrusion starts to shrink back to the rounded shape of the nozzle + // The anchored part not, because the melted material holds to the previous layer well. + // We can assume for simplicity perfect equalization of layer height and raising part width, from which: + float swelling_radius = (layer_height + curling_section) / 2.0f; + curled_up_height += std::max(0.f, (swelling_radius - layer_height) / 2.0f); + + // There is one more effect. On convex turns, there is larger tension on the floating edge of the extrusion then on the middle section. + // The tension is caused by the shrinking tendency of the filament, and on outer edge of convex trun, the expansion is greater and thus shrinking force is greater. + // This tension will cause the curling section to curle up (Why not down? maybe the previous layer works as a heat block, releasing the heat + // faster or slower than thin air, thus the extrusion always curles up) + + if (point.curvature > 0.01){ + float radius = 1.0 / point.curvature; + // compute radius at the point where the extrusion stops touch previous layer and starts curling + float radius_anchored_section_end = radius - flow_width / 2.0 + anchored_section; + // target radius represents the radius of the extrusion curling end, after curling + // the layer_height term aproximates that the extrusion curling part, when raising to vertical position, will stop before reaching + // perpendicular position, due to various forces. + float target_radius = std::max(radius, radius_anchored_section_end) + layer_height; + + float b = target_radius - radius_anchored_section_end; + float a = (curling_section + swelling_radius) / 2.0; + float c = sqrt(a*a - b*b); + + curled_up_height += c; + } curled_up_height = std::min(curled_up_height, params.max_curled_height_factor * layer_height); } diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index 543b9c92b..c673fc21f 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -42,7 +42,7 @@ struct Params BrimType brim_type; const float brim_width; - const std::pair malformation_distance_factors = std::pair { 0.5, 1.1 }; + const std::pair malformation_distance_factors = std::pair { 0.3, 0.9 }; const float max_curled_height_factor = 10.0f; const float curling_tolerance_limit = 0.1f; From 3e42d16f622985c840c9f4446917ffde328f62be Mon Sep 17 00:00:00 2001 From: Pavel Mikus Date: Wed, 29 Mar 2023 18:32:41 +0200 Subject: [PATCH 047/115] minor parameter changes --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 7 +++---- src/libslic3r/SupportSpotsGenerator.hpp | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 98febf609..bf59543dd 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -299,13 +299,12 @@ public: for (ExtendedPoint& ep : extended_points) { // We are going to enforce slowdown by increasing the point distance. The overhang speed is based on signed distance from - // the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width and more means full overhang, thus full slowdown. - // However, for curling, we take unsinged distance from the curled lines and artifically modifiy the distance + // the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width and more means full overhang, + // thus full slowdown. However, for curling, we take unsinged distance from the curled lines and artifically modifiy the distance float distance_from_curled = prev_curled_extrusions[current_object].distance_from_lines(ep.position); - ep.distance = std::max(ep.distance, path.width - distance_from_curled); + ep.distance = std::max(ep.distance, (path.width - distance_from_curled)); } - std::vector processed_points; processed_points.reserve(extended_points.size()); for (size_t i = 0; i < extended_points.size(); i++) { diff --git a/src/libslic3r/SupportSpotsGenerator.hpp b/src/libslic3r/SupportSpotsGenerator.hpp index c673fc21f..a946159b9 100644 --- a/src/libslic3r/SupportSpotsGenerator.hpp +++ b/src/libslic3r/SupportSpotsGenerator.hpp @@ -42,7 +42,7 @@ struct Params BrimType brim_type; const float brim_width; - const std::pair malformation_distance_factors = std::pair { 0.3, 0.9 }; + const std::pair malformation_distance_factors = std::pair { 0.2, 1.1 }; const float max_curled_height_factor = 10.0f; const float curling_tolerance_limit = 0.1f; From 4ade7d6e8c52773072f6befe8e50beec9425649e Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 31 Mar 2023 12:48:47 +0200 Subject: [PATCH 048/115] Some improvements of the algortihm for curled height estim --- src/libslic3r/SupportSpotsGenerator.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index bef3c0fc1..281d72948 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -210,8 +210,8 @@ float estimate_curled_up_height( const ExtendedPoint &point, float layer_height, float flow_width, float prev_line_curled_height, Params params) { float curled_up_height = 0; - if (fabs(point.distance) < 1.5 * flow_width) { - curled_up_height = 0.9 * prev_line_curled_height; + if (fabs(point.distance) < 1.1 * flow_width) { + curled_up_height = std::max(prev_line_curled_height - layer_height * 0.5, 0.0); } if (point.distance > params.malformation_distance_factors.first * flow_width && @@ -233,17 +233,17 @@ float estimate_curled_up_height( // faster or slower than thin air, thus the extrusion always curles up) if (point.curvature > 0.01){ - float radius = 1.0 / point.curvature; - // compute radius at the point where the extrusion stops touch previous layer and starts curling - float radius_anchored_section_end = radius - flow_width / 2.0 + anchored_section; + float radius = std::max(1.0 / point.curvature - flow_width / 2.0, 0.001); + // compute radius at the point where the extrusion stops touching the previous layer and starts curling + float radius_anchored_section_end = radius + anchored_section; // target radius represents the radius of the extrusion curling end, after curling // the layer_height term aproximates that the extrusion curling part, when raising to vertical position, will stop before reaching // perpendicular position, due to various forces. - float target_radius = std::max(radius, radius_anchored_section_end) + layer_height; + float target_radius = radius_anchored_section_end + radius * flow_width / 100.0; float b = target_radius - radius_anchored_section_end; - float a = (curling_section + swelling_radius) / 2.0; - float c = sqrt(a*a - b*b); + float a = curling_section; + float c = sqrt(std::max(0.0f,a*a - b*b)); curled_up_height += c; } From 6ee674316dd76fc74fd4007fb4e5fcd3fa8dce33 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 3 Apr 2023 10:10:15 +0200 Subject: [PATCH 049/115] Finally some working curling model --- src/libslic3r/SupportSpotsGenerator.cpp | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 281d72948..ee62b1e6c 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -218,7 +218,7 @@ float estimate_curled_up_height( point.distance < params.malformation_distance_factors.second * flow_width) { // imagine the extrusion profile. The part that has been glued (melted) with the previous layer will be called anchored section // and the rest will be called curling section - float anchored_section = flow_width - point.distance; + // float anchored_section = flow_width - point.distance; float curling_section = point.distance; // after extruding, the curling (floating) part of the extrusion starts to shrink back to the rounded shape of the nozzle @@ -234,14 +234,8 @@ float estimate_curled_up_height( if (point.curvature > 0.01){ float radius = std::max(1.0 / point.curvature - flow_width / 2.0, 0.001); - // compute radius at the point where the extrusion stops touching the previous layer and starts curling - float radius_anchored_section_end = radius + anchored_section; - // target radius represents the radius of the extrusion curling end, after curling - // the layer_height term aproximates that the extrusion curling part, when raising to vertical position, will stop before reaching - // perpendicular position, due to various forces. - float target_radius = radius_anchored_section_end + radius * flow_width / 100.0; - - float b = target_radius - radius_anchored_section_end; + float curling_t = radius / 100; + float b = curling_t * flow_width; float a = curling_section; float c = sqrt(std::max(0.0f,a*a - b*b)); From 6e40e061f6fa057767ffe2a7916f8e175c48e4f6 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Mon, 3 Apr 2023 15:14:07 +0200 Subject: [PATCH 050/115] Finish rough implementation of slowdown over curled filament --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 33 ++++++++++++---------- src/libslic3r/JumpPointSearch.cpp | 4 +-- src/libslic3r/Layer.hpp | 3 +- src/libslic3r/Line.hpp | 12 ++++++++ src/libslic3r/SupportSpotsGenerator.cpp | 12 ++++---- 5 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index bf59543dd..ffa351c27 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -13,6 +13,7 @@ #include "../ClipperUtils.hpp" #include "../Flow.hpp" #include "../Config.hpp" +#include "../Line.hpp" #include #include @@ -246,8 +247,8 @@ class ExtrusionQualityEstimator { std::unordered_map> prev_layer_boundaries; std::unordered_map> next_layer_boundaries; - std::unordered_map> prev_curled_extrusions; - std::unordered_map> next_curled_extrusions; + std::unordered_map> prev_curled_extrusions; + std::unordered_map> next_curled_extrusions; const PrintObject *current_object; public: @@ -261,12 +262,7 @@ public: prev_layer_boundaries[object] = next_layer_boundaries[object]; next_layer_boundaries[object] = AABBTreeLines::LinesDistancer{to_unscaled_linesf(layer->lslices)}; prev_curled_extrusions[object] = next_curled_extrusions[object]; - Linesf curled_lines; - curled_lines.reserve(layer->malformed_lines.size()); - for (const Line &l : layer->malformed_lines) { - curled_lines.push_back(Linef(unscaled(l.a), unscaled(l.b))); - } - next_curled_extrusions[object] = AABBTreeLines::LinesDistancer{curled_lines}; + next_curled_extrusions[object] = AABBTreeLines::LinesDistancer{layer->curled_lines}; } std::vector estimate_speed_from_extrusion_quality( @@ -296,13 +292,20 @@ public: std::vector extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width); - - for (ExtendedPoint& ep : extended_points) { - // We are going to enforce slowdown by increasing the point distance. The overhang speed is based on signed distance from - // the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width and more means full overhang, - // thus full slowdown. However, for curling, we take unsinged distance from the curled lines and artifically modifiy the distance - float distance_from_curled = prev_curled_extrusions[current_object].distance_from_lines(ep.position); - ep.distance = std::max(ep.distance, (path.width - distance_from_curled)); + + for (ExtendedPoint &ep : extended_points) { + // We are going to enforce slowdown over curled extrusions by increasing the point distance. The overhang speed is based on + // signed distance from the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width + // and more means full overhang, thus full slowdown. However, for curling, we take unsinged distance from the curled lines and + // artifically modifiy the distance + auto [distance_from_curled, line_idx, + p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(ep.position)); + if (distance_from_curled < scale_(2.0 * path.width)) { + float articifally_increased_distance = path.width * + prev_curled_extrusions[current_object].get_line(line_idx).curled_height / + (path.height * 10.0f); // max_curled_height_factor from SupportSpotGenerator + ep.distance = std::max(ep.distance, articifally_increased_distance); + } } std::vector processed_points; diff --git a/src/libslic3r/JumpPointSearch.cpp b/src/libslic3r/JumpPointSearch.cpp index ef3dba45e..dbae42151 100644 --- a/src/libslic3r/JumpPointSearch.cpp +++ b/src/libslic3r/JumpPointSearch.cpp @@ -211,8 +211,8 @@ void JPSPathFinder::add_obstacles(const Layer *layer, const Point &global_origin this->print_z = layer->print_z; Lines obstacles; - obstacles.reserve(layer->malformed_lines.size()); - for (const Line &l : layer->malformed_lines) { obstacles.push_back(Line{l.a + global_origin, l.b + global_origin}); } + obstacles.reserve(layer->curled_lines.size()); + for (const Line &l : layer->curled_lines) { obstacles.push_back(Line{l.a + global_origin, l.b + global_origin}); } add_obstacles(obstacles); } diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index b3d071c9d..5cfdf9cfa 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -1,6 +1,7 @@ #ifndef slic3r_Layer_hpp_ #define slic3r_Layer_hpp_ +#include "Line.hpp" #include "libslic3r.h" #include "BoundingBox.hpp" #include "Flow.hpp" @@ -325,7 +326,7 @@ public: coordf_t bottom_z() const { return this->print_z - this->height; } //Extrusions estimated to be seriously malformed, estimated during "Estimating curled extrusions" step. These lines should be avoided during fast travels. - Lines malformed_lines; + CurledLines curled_lines; // Collection of expolygons generated by slicing the possibly multiple meshes of the source geometry // (with possibly differing extruder ID and slicing parameters) and merged. diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index 90f564898..1edd1f5e9 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -209,6 +209,18 @@ public: double a_width, b_width; }; +class CurledLine : public Line +{ +public: + CurledLine() : curled_height(0.0f) {} + CurledLine(const Point& a, const Point& b) : Line(a, b), curled_height(0.0f) {} + CurledLine(const Point& a, const Point& b, float curled_height) : Line(a, b), curled_height(curled_height) {} + + float curled_height; +}; + +using CurledLines = std::vector; + class Line3 { public: diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index ee62b1e6c..71ea7264d 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -39,7 +39,7 @@ #include "Geometry/ConvexHull.hpp" // #define DETAILED_DEBUG_LOGS -#define DEBUG_FILES +// #define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -234,7 +234,7 @@ float estimate_curled_up_height( if (point.curvature > 0.01){ float radius = std::max(1.0 / point.curvature - flow_width / 2.0, 0.001); - float curling_t = radius / 100; + float curling_t = sqrt(radius / 100); float b = curling_t * flow_width; float a = curling_section; float c = sqrt(std::max(0.0f,a*a - b*b)); @@ -1066,7 +1066,7 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, AABBTreeLines::LinesDistancer prev_layer_lines{}; for (SupportLayer *l : layers) { - l->malformed_lines.clear(); + l->curled_lines.clear(); std::vector current_layer_lines; for (const ExtrusionEntity *extrusion : l->support_fills.flatten().entities) { @@ -1102,7 +1102,7 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, for (const ExtrusionLine &line : current_layer_lines) { if (line.curled_up_height > params.curling_tolerance_limit) { - l->malformed_lines.push_back(Line{Point::new_scale(line.a), Point::new_scale(line.b)}); + l->curled_lines.push_back(CurledLine{Point::new_scale(line.a), Point::new_scale(line.b), line.curled_up_height}); } } @@ -1138,7 +1138,7 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) LD prev_layer_lines{}; for (Layer *l : layers) { - l->malformed_lines.clear(); + l->curled_lines.clear(); std::vector boundary_lines = l->lower_layer != nullptr ? to_unscaled_linesf(l->lower_layer->lslices) : std::vector(); AABBTreeLines::LinesDistancer prev_layer_boundary{std::move(boundary_lines)}; std::vector current_layer_lines; @@ -1176,7 +1176,7 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) for (const ExtrusionLine &line : current_layer_lines) { if (line.curled_up_height > params.curling_tolerance_limit) { - l->malformed_lines.push_back(Line{Point::new_scale(line.a), Point::new_scale(line.b)}); + l->curled_lines.push_back(CurledLine{Point::new_scale(line.a), Point::new_scale(line.b), line.curled_up_height}); } } From da6b972a79843ab5886d67dd5959e6a503313548 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 11 Apr 2023 13:01:31 +0200 Subject: [PATCH 051/115] Smoothen the curled height estimations and slowdown --- src/libslic3r/AABBTreeLines.hpp | 2 +- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/GCode/ExtrusionProcessor.hpp | 45 ++++++++++++++++------ src/libslic3r/SupportSpotsGenerator.cpp | 44 ++++++++++++++++----- 4 files changed, 69 insertions(+), 24 deletions(-) diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index 2136e8edb..ae5569b51 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -347,7 +347,7 @@ public: std::vector all_lines_in_radius(const Vec<2, typename LineType::Scalar> &point, Floating radius) { - return all_lines_in_radius(this->lines, this->tree, point, radius * radius); + return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point, radius * radius); } template std::vector, size_t>> intersections_with_line(const LineType &line) const diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 29316a9de..357071a61 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3036,7 +3036,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string_view de EXTRUDER_CONFIG(filament_max_volumetric_speed) / path.mm3_per_mm); } - new_points = m_extrusion_quality_estimator.estimate_extrusion_quality(path, overhangs_with_speeds, overhang_w_fan_speeds, + new_points = m_extrusion_quality_estimator.estimate_speed_from_extrusion_quality(path, overhangs_with_speeds, overhang_w_fan_speeds, m_writer.extruder()->id(), external_perim_reference_speed, speed); variable_speed_or_fan_speed = std::any_of(new_points.begin(), new_points.end(), diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index ffa351c27..81a1c018a 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -101,13 +101,10 @@ public: struct ExtendedPoint { - ExtendedPoint(Vec2d position, float distance = 0.0, size_t nearest_prev_layer_line = size_t(-1), float curvature = 0.0) - : position(position), distance(distance), nearest_prev_layer_line(nearest_prev_layer_line), curvature(curvature) - {} - Vec2d position; float distance; size_t nearest_prev_layer_line; + Vec2d nearest_prev_layer_point; float curvature; }; @@ -132,6 +129,7 @@ std::vector estimate_points_properties(const std::vector

auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(start_point.position.cast()); start_point.distance = distance + boundary_offset; start_point.nearest_prev_layer_line = nearest_line; + start_point.nearest_prev_layer_point = x.template cast(); points.push_back(start_point); } for (size_t i = 1; i < input_points.size(); i++) { @@ -139,13 +137,19 @@ std::vector estimate_points_properties(const std::vector

auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.cast()); next_point.distance = distance + boundary_offset; next_point.nearest_prev_layer_line = nearest_line; + next_point.nearest_prev_layer_point = x.template cast(); if (ADD_INTERSECTIONS && ((points.back().distance > boundary_offset + EPSILON) != (next_point.distance > boundary_offset + EPSILON))) { const ExtendedPoint &prev_point = points.back(); auto intersections = unscaled_prev_layer.template intersections_with_line(L{prev_point.position.cast(), next_point.position.cast()}); for (const auto &intersection : intersections) { - points.emplace_back(intersection.first.template cast(), boundary_offset, intersection.second); + ExtendedPoint p{}; + p.position = intersection.first.template cast(); + p.distance = boundary_offset; + p.nearest_prev_layer_line = intersection.second; + p.nearest_prev_layer_point = p.position; + points.push_back(p); } } points.push_back(next_point); @@ -171,12 +175,22 @@ std::vector estimate_points_properties(const std::vector

if (t0 < 1.0) { auto p0 = curr.position + t0 * (next.position - curr.position); auto [p0_dist, p0_near_l, p0_x] = unscaled_prev_layer.template distance_from_lines_extra(p0.cast()); - new_points.push_back(ExtendedPoint{p0, float(p0_dist + boundary_offset), p0_near_l}); + ExtendedPoint new_p{}; + new_p.position = p0; + new_p.distance = float(p0_dist + boundary_offset); + new_p.nearest_prev_layer_line = p0_near_l; + new_p.nearest_prev_layer_point = p0_x.template cast(); + new_points.push_back(new_p); } if (t1 > 0.0) { auto p1 = curr.position + t1 * (next.position - curr.position); auto [p1_dist, p1_near_l, p1_x] = unscaled_prev_layer.template distance_from_lines_extra(p1.cast()); - new_points.push_back(ExtendedPoint{p1, float(p1_dist + boundary_offset), p1_near_l}); + ExtendedPoint new_p{}; + new_p.position = p1; + new_p.distance = float(p1_dist + boundary_offset); + new_p.nearest_prev_layer_line = p1_near_l; + new_p.nearest_prev_layer_point = p1_x.template cast(); + new_points.push_back(new_p); } } } @@ -200,7 +214,12 @@ std::vector estimate_points_properties(const std::vector

Vec2d pos = curr.position * (1.0 - j * t) + next.position * (j * t); auto [p_dist, p_near_l, p_x] = unscaled_prev_layer.template distance_from_lines_extra(pos.cast()); - new_points.push_back(ExtendedPoint{pos, float(p_dist + boundary_offset), p_near_l}); + ExtendedPoint new_p{}; + new_p.position = pos; + new_p.distance = float(p_dist + boundary_offset); + new_p.nearest_prev_layer_line = p_near_l; + new_p.nearest_prev_layer_point = p_x.template cast(); + new_points.push_back(new_p); } } new_points.push_back(points.back()); @@ -301,10 +320,12 @@ public: auto [distance_from_curled, line_idx, p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(ep.position)); if (distance_from_curled < scale_(2.0 * path.width)) { - float articifally_increased_distance = path.width * - prev_curled_extrusions[current_object].get_line(line_idx).curled_height / - (path.height * 10.0f); // max_curled_height_factor from SupportSpotGenerator - ep.distance = std::max(ep.distance, articifally_increased_distance); + float artificially_increased_distance = path.width * + (1.0 - (unscaled(distance_from_curled) / (2.0 * path.width)) * + (unscaled(distance_from_curled) / (2.0 * path.width))) * + (prev_curled_extrusions[current_object].get_line(line_idx).curled_height / + (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator + ep.distance = std::max(ep.distance, artificially_increased_distance); } } diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 71ea7264d..15a4aa535 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -13,6 +13,7 @@ #include "PrintBase.hpp" #include "PrintConfig.hpp" #include "Tesselate.hpp" +#include "Utils.hpp" #include "libslic3r.h" #include "tbb/parallel_for.h" #include "tbb/blocked_range.h" @@ -247,6 +248,32 @@ float estimate_curled_up_height( return curled_up_height; } +std::tuple get_bottom_extrusions_quality_and_curling(const LD &prev_layer_lines, const ExtendedPoint &curr_point) +{ + if (prev_layer_lines.get_lines().empty()) { + return {1.0,0.0}; + } + const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_line(curr_point.nearest_prev_layer_line); + float quality = nearest_prev_layer_line.form_quality; + float curling = nearest_prev_layer_line.curled_up_height; + if ((curr_point.nearest_prev_layer_point.cast() - nearest_prev_layer_line.a).squaredNorm() < 0.1) { + const auto& prev_line = prev_layer_lines.get_line(prev_idx_modulo(curr_point.nearest_prev_layer_line, prev_layer_lines.get_lines().size())); + if ((curr_point.nearest_prev_layer_point.cast() - prev_line.b).squaredNorm() < 0.1) { + quality = 0.5 * (quality + prev_line.form_quality); + curling = 0.5 * (curling + prev_line.curled_up_height); + } + } else if ((curr_point.nearest_prev_layer_point.cast() - nearest_prev_layer_line.b).squaredNorm() < 0.1) { + const auto &next_line = prev_layer_lines.get_line( + next_idx_modulo(curr_point.nearest_prev_layer_line, prev_layer_lines.get_lines().size())); + if ((curr_point.nearest_prev_layer_point.cast() - next_line.a).squaredNorm() < 0.1) { + quality = 0.5 * (quality + next_line.form_quality); + curling = 0.5 * (curling + next_line.curled_up_height); + } + } + + return {quality, curling}; +} + std::vector check_extrusion_entity_stability(const ExtrusionEntity *entity, const LayerRegion *layer_region, const LD &prev_layer_lines, @@ -335,9 +362,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit float line_len = (prev_point.position - curr_point.position).norm(); ExtrusionLine line_out{prev_point.position.cast(), curr_point.position.cast(), line_len, entity}; - const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? - prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : - ExtrusionLine{}; + auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); // correctify the distance sign using slice polygons float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; @@ -362,7 +387,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } else if (curr_point.distance > flow_width * 0.8f) { bridged_distance += line_len; - line_out.form_quality = nearest_prev_layer_line.form_quality - 0.3f; + line_out.form_quality = prev_layer_quality - 0.3f; if (line_out.form_quality < 0 && bridged_distance > max_bridge_len) { line_out.support_point_generated = potential_cause; line_out.form_quality = 0.5f; @@ -373,7 +398,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } line_out.curled_up_height = estimate_curled_up_height(curr_point, layer_region->layer()->height, flow_width, - nearest_prev_layer_line.curled_up_height, params); + prev_layer_curling, params); lines_out.push_back(line_out); } @@ -1082,6 +1107,7 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast() : curr_point.position.cast(), curr_point.position.cast(), line_len, extrusion}; + auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : ExtrusionLine{}; @@ -1094,7 +1120,7 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, } line_out.curled_up_height = estimate_curled_up_height(curr_point, l->height, flow_width, - nearest_prev_layer_line.curled_up_height, params); + prev_layer_curling, params); current_layer_lines.push_back(line_out); } @@ -1158,16 +1184,14 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast() : curr_point.position.cast(), curr_point.position.cast(), line_len, extrusion}; - const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? - prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : - ExtrusionLine{}; + auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; curr_point.distance *= sign; line_out.curled_up_height = estimate_curled_up_height(curr_point, layer_region->layer()->height, flow_width, - nearest_prev_layer_line.curled_up_height, params); + prev_layer_curling, params); current_layer_lines.push_back(line_out); } From aa0e21eed1f6eda7ce93f26821edb5cc8bcbbf7c Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Tue, 11 Apr 2023 16:40:14 +0200 Subject: [PATCH 052/115] Center estiamted curvature values, Center esimtated curling - transfering data by nearest line caused CCW or CW shift, based on which point of the current line was used. By switching the points on layer basis, this problem disapeared --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 127 ++++++++------------- src/libslic3r/SupportSpotsGenerator.cpp | 60 +++++----- 2 files changed, 77 insertions(+), 110 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 81a1c018a..6e441a4c6 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -27,78 +27,6 @@ namespace Slic3r { -class SlidingWindowCurvatureAccumulator -{ - float window_size; - float total_distance = 0; // accumulated distance - float total_curvature = 0; // accumulated signed ccw angles - deque distances; - deque angles; - -public: - SlidingWindowCurvatureAccumulator(float window_size) : window_size(window_size) {} - - void add_point(float distance, float angle) - { - total_distance += distance; - total_curvature += angle; - distances.push_back(distance); - angles.push_back(angle); - - while (distances.size() > 1 && total_distance > window_size) { - total_distance -= distances.front(); - total_curvature -= angles.front(); - distances.pop_front(); - angles.pop_front(); - } - } - - float get_curvature() const - { - return total_curvature / window_size; - } - - void reset() - { - total_curvature = 0; - total_distance = 0; - distances.clear(); - angles.clear(); - } -}; - -class CurvatureEstimator -{ - static const size_t sliders_count = 3; - SlidingWindowCurvatureAccumulator sliders[sliders_count] = {{1.0},{4.0}, {10.0}}; - -public: - void add_point(float distance, float angle) - { - if (distance < EPSILON) - return; - for (SlidingWindowCurvatureAccumulator &slider : sliders) { - slider.add_point(distance, angle); - } - } - float get_curvature() - { - float max_curvature = 0.0f; - for (const SlidingWindowCurvatureAccumulator &slider : sliders) { - if (abs(slider.get_curvature()) > abs(max_curvature)) { - max_curvature = slider.get_curvature(); - } - } - return max_curvature; - } - void reset() - { - for (SlidingWindowCurvatureAccumulator &slider : sliders) { - slider.reset(); - } - } -}; - struct ExtendedPoint { Vec2d position; @@ -118,7 +46,6 @@ std::vector estimate_points_properties(const std::vector

if (input_points.empty()) return {}; float boundary_offset = PREV_LAYER_BOUNDARY_OFFSET ? 0.5 * flow_width : 0.0f; - CurvatureEstimator cestim; auto maybe_unscale = [](const P &p) { return SCALED_INPUT ? unscaled(p) : p.template cast(); }; std::vector points; @@ -227,6 +154,9 @@ std::vector estimate_points_properties(const std::vector

points = new_points; } + std::vector angles_for_curvature(points.size()); + std::vector distances_for_curvature(points.size()); + for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { ExtendedPoint &a = points[point_idx]; ExtendedPoint &prev = points[point_idx > 0 ? point_idx - 1 : point_idx]; @@ -234,22 +164,59 @@ std::vector estimate_points_properties(const std::vector

int prev_point_idx = point_idx; while (prev_point_idx > 0) { prev_point_idx--; - if ((a.position - points[prev_point_idx].position).squaredNorm() > EPSILON) { break; } + if ((a.position - points[prev_point_idx].position).squaredNorm() > EPSILON) { + break; + } } int next_point_index = point_idx; while (next_point_index < int(points.size()) - 1) { next_point_index++; - if ((a.position - points[next_point_index].position).squaredNorm() > EPSILON) { break; } + if ((a.position - points[next_point_index].position).squaredNorm() > EPSILON) { + break; + } } + distances_for_curvature[point_idx] = (prev.position - a.position).norm(); if (prev_point_idx != point_idx && next_point_index != point_idx) { - float distance = (prev.position - a.position).norm(); - float alfa = angle(a.position - points[prev_point_idx].position, points[next_point_index].position - a.position); - cestim.add_point(distance, alfa); - } + float alfa = angle(a.position - points[prev_point_idx].position, points[next_point_index].position - a.position); + angles_for_curvature[point_idx] = alfa; + } // else keep zero + } - a.curvature = cestim.get_curvature(); + for (float window_size : {3.0f, 9.0f, 16.0f}) { + size_t tail_point = 0; + float tail_window_acc = 0; + float tail_angle_acc = 0; + + size_t head_point = 0; + float head_window_acc = 0; + float head_angle_acc = 0; + + for (int point_idx = 0; point_idx < int(points.size()); ++point_idx) { + if (point_idx > 0) { + tail_window_acc += distances_for_curvature[point_idx - 1]; + tail_angle_acc += angles_for_curvature[point_idx - 1]; + head_window_acc -= distances_for_curvature[point_idx - 1]; + head_angle_acc -= angles_for_curvature[point_idx - 1]; + } + while (tail_window_acc > window_size * 0.5 && tail_point < point_idx) { + tail_window_acc -= distances_for_curvature[tail_point]; + tail_angle_acc -= angles_for_curvature[tail_point]; + tail_point++; + } + + while (head_window_acc < window_size * 0.5 && head_point < int(points.size()) - 1) { + head_window_acc += distances_for_curvature[head_point]; + head_angle_acc += angles_for_curvature[head_point]; + head_point++; + } + + float curvature = (tail_angle_acc + head_angle_acc) / (tail_window_acc + head_window_acc); + if (std::abs(curvature) > std::abs(points[point_idx].curvature)) { + points[point_idx].curvature = curvature; + } + } } return points; diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 15a4aa535..0ad76d8b1 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -234,7 +234,7 @@ float estimate_curled_up_height( // faster or slower than thin air, thus the extrusion always curles up) if (point.curvature > 0.01){ - float radius = std::max(1.0 / point.curvature - flow_width / 2.0, 0.001); + float radius = (1.0 / point.curvature); float curling_t = sqrt(radius / 100); float b = curling_t * flow_width; float a = curling_section; @@ -397,8 +397,8 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit bridged_distance = 0.0f; } - line_out.curled_up_height = estimate_curled_up_height(curr_point, layer_region->layer()->height, flow_width, - prev_layer_curling, params); + line_out.curled_up_height = estimate_curled_up_height(layer_region->layer()->id() % 2 == 0 ? curr_point : prev_point, + layer_region->layer()->height, flow_width, prev_layer_curling, params); lines_out.push_back(line_out); } @@ -1102,25 +1102,25 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, auto annotated_points = estimate_points_properties(pol.points, prev_layer_lines, flow_width); for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &curr_point = annotated_points[i]; - float line_len = i > 0 ? ((annotated_points[i - 1].position - curr_point.position).norm()) : 0.0f; - ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast() : curr_point.position.cast(), - curr_point.position.cast(), line_len, extrusion}; + ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + ExtendedPoint &b = annotated_points[i]; + ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), + extrusion}; - auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); - const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? - prev_layer_lines.get_line(curr_point.nearest_prev_layer_line) : - ExtrusionLine{}; + ExtendedPoint &pivot = l->id() % 2 == 0 ? a : b; + auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, pivot); + const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? + prev_layer_lines.get_line(pivot.nearest_prev_layer_line) : + ExtrusionLine{}; Vec2f v1 = (nearest_prev_layer_line.b - nearest_prev_layer_line.a); - Vec2f v2 = (curr_point.position.cast() - nearest_prev_layer_line.a); + Vec2f v2 = (pivot.position.cast() - nearest_prev_layer_line.a); auto d = (v1.x() * v2.y()) - (v1.y() * v2.x()); if (d > 0) { - curr_point.distance *= -1.0f; + pivot.distance *= -1.0f; } - line_out.curled_up_height = estimate_curled_up_height(curr_point, l->height, flow_width, - prev_layer_curling, params); + line_out.curled_up_height = estimate_curled_up_height(pivot, l->height, flow_width, prev_layer_curling, params); current_layer_lines.push_back(line_out); } @@ -1158,7 +1158,7 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) { #ifdef DEBUG_FILES FILE *debug_file = boost::nowide::fopen(debug_out_path("object_malformations.obj").c_str(), "w"); - FILE *full_file = boost::nowide::fopen(debug_out_path("object_full.obj").c_str(), "w"); + FILE *full_file = boost::nowide::fopen(debug_out_path("object_full.obj").c_str(), "w"); #endif LD prev_layer_lines{}; @@ -1170,8 +1170,8 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) std::vector current_layer_lines; for (const LayerRegion *layer_region : l->regions()) { for (const ExtrusionEntity *extrusion : layer_region->perimeters().flatten().entities) { - - if (!extrusion->role().is_external_perimeter()) continue; + if (!extrusion->role().is_external_perimeter()) + continue; Points extrusion_pts; extrusion->collect_points(extrusion_pts); @@ -1179,18 +1179,18 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) auto annotated_points = estimate_points_properties(extrusion_pts, prev_layer_lines, flow_width, params.bridge_distance); for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &curr_point = annotated_points[i]; - float line_len = i > 0 ? ((annotated_points[i - 1].position - curr_point.position).norm()) : 0.0f; - ExtrusionLine line_out{i > 0 ? annotated_points[i - 1].position.cast() : curr_point.position.cast(), - curr_point.position.cast(), line_len, extrusion}; + ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + ExtendedPoint &b = annotated_points[i]; + ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), + extrusion}; - auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); + ExtendedPoint &pivot = l->id() % 2 == 0 ? a : b; + auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, pivot); - float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : - 1.0f; - curr_point.distance *= sign; + float sign = (prev_layer_boundary.distance_from_lines(pivot.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; + pivot.distance *= sign; - line_out.curled_up_height = estimate_curled_up_height(curr_point, layer_region->layer()->height, flow_width, + line_out.curled_up_height = estimate_curled_up_height(pivot, layer_region->layer()->height, flow_width, prev_layer_curling, params); current_layer_lines.push_back(line_out); @@ -1211,9 +1211,9 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) fprintf(debug_file, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], l->print_z, color[0], color[1], color[2]); } } - for (const ExtrusionLine &line : current_layer_lines) { - Vec3f color = value_to_rgbf(-EPSILON, l->height * params.max_curled_height_factor, line.curled_up_height); - fprintf(full_file, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], l->print_z, color[0], color[1], color[2]); + for (const ExtrusionLine &line : current_layer_lines) { + Vec3f color = value_to_rgbf(-EPSILON, l->height * params.max_curled_height_factor, line.curled_up_height); + fprintf(full_file, "v %f %f %f %f %f %f\n", line.b[0], line.b[1], l->print_z, color[0], color[1], color[2]); } #endif From 798396d9181ed5402cc377d868caf03de2689885 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Wed, 12 Apr 2023 17:24:11 +0200 Subject: [PATCH 053/115] Fixed several issues with smoothening of the slowdown, but there are still artefacts in the preview, on curved into flat srufaces --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 20 +--- src/libslic3r/SupportSpotsGenerator.cpp | 122 +++++++++------------ 2 files changed, 53 insertions(+), 89 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index 6e441a4c6..d79fee83a 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -31,8 +31,6 @@ struct ExtendedPoint { Vec2d position; float distance; - size_t nearest_prev_layer_line; - Vec2d nearest_prev_layer_point; float curvature; }; @@ -55,16 +53,12 @@ std::vector estimate_points_properties(const std::vector

ExtendedPoint start_point{maybe_unscale(input_points.front())}; auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(start_point.position.cast()); start_point.distance = distance + boundary_offset; - start_point.nearest_prev_layer_line = nearest_line; - start_point.nearest_prev_layer_point = x.template cast(); points.push_back(start_point); } for (size_t i = 1; i < input_points.size(); i++) { ExtendedPoint next_point{maybe_unscale(input_points[i])}; auto [distance, nearest_line, x] = unscaled_prev_layer.template distance_from_lines_extra(next_point.position.cast()); next_point.distance = distance + boundary_offset; - next_point.nearest_prev_layer_line = nearest_line; - next_point.nearest_prev_layer_point = x.template cast(); if (ADD_INTERSECTIONS && ((points.back().distance > boundary_offset + EPSILON) != (next_point.distance > boundary_offset + EPSILON))) { @@ -74,8 +68,6 @@ std::vector estimate_points_properties(const std::vector

ExtendedPoint p{}; p.position = intersection.first.template cast(); p.distance = boundary_offset; - p.nearest_prev_layer_line = intersection.second; - p.nearest_prev_layer_point = p.position; points.push_back(p); } } @@ -105,8 +97,6 @@ std::vector estimate_points_properties(const std::vector

ExtendedPoint new_p{}; new_p.position = p0; new_p.distance = float(p0_dist + boundary_offset); - new_p.nearest_prev_layer_line = p0_near_l; - new_p.nearest_prev_layer_point = p0_x.template cast(); new_points.push_back(new_p); } if (t1 > 0.0) { @@ -115,8 +105,6 @@ std::vector estimate_points_properties(const std::vector

ExtendedPoint new_p{}; new_p.position = p1; new_p.distance = float(p1_dist + boundary_offset); - new_p.nearest_prev_layer_line = p1_near_l; - new_p.nearest_prev_layer_point = p1_x.template cast(); new_points.push_back(new_p); } } @@ -144,8 +132,6 @@ std::vector estimate_points_properties(const std::vector

ExtendedPoint new_p{}; new_p.position = pos; new_p.distance = float(p_dist + boundary_offset); - new_p.nearest_prev_layer_line = p_near_l; - new_p.nearest_prev_layer_point = p_x.template cast(); new_points.push_back(new_p); } } @@ -279,13 +265,15 @@ public: std::vector extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width); - for (ExtendedPoint &ep : extended_points) { + for (size_t i = 0; i < int(extended_points.size()) - 1; i++) { + ExtendedPoint& ep = extended_points[i]; // We are going to enforce slowdown over curled extrusions by increasing the point distance. The overhang speed is based on // signed distance from the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width // and more means full overhang, thus full slowdown. However, for curling, we take unsinged distance from the curled lines and // artifically modifiy the distance + Vec2d middle = 0.5 * (ep.position + extended_points[i + 1].position); auto [distance_from_curled, line_idx, - p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(ep.position)); + p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(middle)); if (distance_from_curled < scale_(2.0 * path.width)) { float artificially_increased_distance = path.width * (1.0 - (unscaled(distance_from_curled) / (2.0 * path.width)) * diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index 0ad76d8b1..e9f60dabd 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -40,7 +41,7 @@ #include "Geometry/ConvexHull.hpp" // #define DETAILED_DEBUG_LOGS -// #define DEBUG_FILES +#define DEBUG_FILES #ifdef DEBUG_FILES #include @@ -208,19 +209,19 @@ std::vector to_short_lines(const ExtrusionEntity *e, float length } float estimate_curled_up_height( - const ExtendedPoint &point, float layer_height, float flow_width, float prev_line_curled_height, Params params) + float distance, float curvature, float layer_height, float flow_width, float prev_line_curled_height, Params params) { float curled_up_height = 0; - if (fabs(point.distance) < 1.1 * flow_width) { - curled_up_height = std::max(prev_line_curled_height - layer_height * 0.5, 0.0); + if (fabs(distance) < 2.5 * flow_width) { + curled_up_height = std::max(prev_line_curled_height - layer_height * 0.75f, 0.0f); } - if (point.distance > params.malformation_distance_factors.first * flow_width && - point.distance < params.malformation_distance_factors.second * flow_width) { + if (distance > params.malformation_distance_factors.first * flow_width && + distance < params.malformation_distance_factors.second * flow_width) { // imagine the extrusion profile. The part that has been glued (melted) with the previous layer will be called anchored section // and the rest will be called curling section // float anchored_section = flow_width - point.distance; - float curling_section = point.distance; + float curling_section = distance; // after extruding, the curling (floating) part of the extrusion starts to shrink back to the rounded shape of the nozzle // The anchored part not, because the melted material holds to the previous layer well. @@ -228,17 +229,15 @@ float estimate_curled_up_height( float swelling_radius = (layer_height + curling_section) / 2.0f; curled_up_height += std::max(0.f, (swelling_radius - layer_height) / 2.0f); - // There is one more effect. On convex turns, there is larger tension on the floating edge of the extrusion then on the middle section. - // The tension is caused by the shrinking tendency of the filament, and on outer edge of convex trun, the expansion is greater and thus shrinking force is greater. - // This tension will cause the curling section to curle up (Why not down? maybe the previous layer works as a heat block, releasing the heat - // faster or slower than thin air, thus the extrusion always curles up) - - if (point.curvature > 0.01){ - float radius = (1.0 / point.curvature); + // On convex turns, there is larger tension on the floating edge of the extrusion then on the middle section. + // The tension is caused by the shrinking tendency of the filament, and on outer edge of convex trun, the expansion is greater and + // thus shrinking force is greater. This tension will cause the curling section to curle up + if (curvature > 0.01) { + float radius = (1.0 / curvature); float curling_t = sqrt(radius / 100); - float b = curling_t * flow_width; - float a = curling_section; - float c = sqrt(std::max(0.0f,a*a - b*b)); + float b = curling_t * flow_width; + float a = curling_section; + float c = sqrt(std::max(0.0f, a * a - b * b)); curled_up_height += c; } @@ -248,32 +247,6 @@ float estimate_curled_up_height( return curled_up_height; } -std::tuple get_bottom_extrusions_quality_and_curling(const LD &prev_layer_lines, const ExtendedPoint &curr_point) -{ - if (prev_layer_lines.get_lines().empty()) { - return {1.0,0.0}; - } - const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_line(curr_point.nearest_prev_layer_line); - float quality = nearest_prev_layer_line.form_quality; - float curling = nearest_prev_layer_line.curled_up_height; - if ((curr_point.nearest_prev_layer_point.cast() - nearest_prev_layer_line.a).squaredNorm() < 0.1) { - const auto& prev_line = prev_layer_lines.get_line(prev_idx_modulo(curr_point.nearest_prev_layer_line, prev_layer_lines.get_lines().size())); - if ((curr_point.nearest_prev_layer_point.cast() - prev_line.b).squaredNorm() < 0.1) { - quality = 0.5 * (quality + prev_line.form_quality); - curling = 0.5 * (curling + prev_line.curled_up_height); - } - } else if ((curr_point.nearest_prev_layer_point.cast() - nearest_prev_layer_line.b).squaredNorm() < 0.1) { - const auto &next_line = prev_layer_lines.get_line( - next_idx_modulo(curr_point.nearest_prev_layer_line, prev_layer_lines.get_lines().size())); - if ((curr_point.nearest_prev_layer_point.cast() - next_line.a).squaredNorm() < 0.1) { - quality = 0.5 * (quality + next_line.form_quality); - curling = 0.5 * (curling + next_line.curled_up_height); - } - } - - return {quality, curling}; -} - std::vector check_extrusion_entity_stability(const ExtrusionEntity *entity, const LayerRegion *layer_region, const LD &prev_layer_lines, @@ -362,7 +335,9 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit float line_len = (prev_point.position - curr_point.position).norm(); ExtrusionLine line_out{prev_point.position.cast(), curr_point.position.cast(), line_len, entity}; - auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, curr_point); + Vec2f middle = 0.5 * (line_out.a + line_out.b); + auto [middle_distance, bottom_line_idx, x] = prev_layer_lines.distance_from_lines_extra(middle); + ExtrusionLine bottom_line = prev_layer_lines.get_lines().empty() ? ExtrusionLine{} : prev_layer_lines.get_line(bottom_line_idx); // correctify the distance sign using slice polygons float sign = (prev_layer_boundary.distance_from_lines(curr_point.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; @@ -387,7 +362,7 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } else if (curr_point.distance > flow_width * 0.8f) { bridged_distance += line_len; - line_out.form_quality = prev_layer_quality - 0.3f; + line_out.form_quality = bottom_line.form_quality - 0.3f; if (line_out.form_quality < 0 && bridged_distance > max_bridge_len) { line_out.support_point_generated = potential_cause; line_out.form_quality = 0.5f; @@ -397,8 +372,9 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit bridged_distance = 0.0f; } - line_out.curled_up_height = estimate_curled_up_height(layer_region->layer()->id() % 2 == 0 ? curr_point : prev_point, - layer_region->layer()->height, flow_width, prev_layer_curling, params); + line_out.curled_up_height = estimate_curled_up_height(middle_distance, 0.5 * (prev_point.curvature + curr_point.curvature), + layer_region->layer()->height, flow_width, bottom_line.curled_up_height, + params); lines_out.push_back(line_out); } @@ -1102,25 +1078,23 @@ void estimate_supports_malformations(SupportLayerPtrs &layers, float flow_width, auto annotated_points = estimate_points_properties(pol.points, prev_layer_lines, flow_width); for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; - ExtendedPoint &b = annotated_points[i]; - ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), + const ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + const ExtendedPoint &b = annotated_points[i]; + ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), extrusion}; - ExtendedPoint &pivot = l->id() % 2 == 0 ? a : b; - auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, pivot); - const ExtrusionLine nearest_prev_layer_line = prev_layer_lines.get_lines().size() > 0 ? - prev_layer_lines.get_line(pivot.nearest_prev_layer_line) : - ExtrusionLine{}; + Vec2f middle = 0.5 * (line_out.a + line_out.b); + auto [middle_distance, bottom_line_idx, x] = prev_layer_lines.distance_from_lines_extra(middle); + ExtrusionLine bottom_line = prev_layer_lines.get_lines().empty() ? ExtrusionLine{} : + prev_layer_lines.get_line(bottom_line_idx); - Vec2f v1 = (nearest_prev_layer_line.b - nearest_prev_layer_line.a); - Vec2f v2 = (pivot.position.cast() - nearest_prev_layer_line.a); - auto d = (v1.x() * v2.y()) - (v1.y() * v2.x()); - if (d > 0) { - pivot.distance *= -1.0f; - } + Vec2f v1 = (bottom_line.b - bottom_line.a); + Vec2f v2 = (a.position.cast() - bottom_line.a); + auto d = (v1.x() * v2.y()) - (v1.y() * v2.x()); + float sign = (d > 0) ? -1.0f : 1.0f; - line_out.curled_up_height = estimate_curled_up_height(pivot, l->height, flow_width, prev_layer_curling, params); + line_out.curled_up_height = estimate_curled_up_height(middle_distance * sign, 0.5 * (a.curvature + b.curvature), l->height, + flow_width, bottom_line.curled_up_height, params); current_layer_lines.push_back(line_out); } @@ -1176,22 +1150,24 @@ void estimate_malformations(LayerPtrs &layers, const Params ¶ms) Points extrusion_pts; extrusion->collect_points(extrusion_pts); float flow_width = get_flow_width(layer_region, extrusion->role()); - auto annotated_points = estimate_points_properties(extrusion_pts, prev_layer_lines, flow_width, - params.bridge_distance); + auto annotated_points = estimate_points_properties(extrusion_pts, prev_layer_lines, flow_width, + params.bridge_distance); for (size_t i = 0; i < annotated_points.size(); ++i) { - ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; - ExtendedPoint &b = annotated_points[i]; - ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), + const ExtendedPoint &a = i > 0 ? annotated_points[i - 1] : annotated_points[i]; + const ExtendedPoint &b = annotated_points[i]; + ExtrusionLine line_out{a.position.cast(), b.position.cast(), float((a.position - b.position).norm()), extrusion}; - ExtendedPoint &pivot = l->id() % 2 == 0 ? a : b; - auto [prev_layer_quality, prev_layer_curling] = get_bottom_extrusions_quality_and_curling(prev_layer_lines, pivot); + Vec2f middle = 0.5 * (line_out.a + line_out.b); + auto [middle_distance, bottom_line_idx, x] = prev_layer_lines.distance_from_lines_extra(middle); + ExtrusionLine bottom_line = prev_layer_lines.get_lines().empty() ? ExtrusionLine{} : + prev_layer_lines.get_line(bottom_line_idx); - float sign = (prev_layer_boundary.distance_from_lines(pivot.position) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; - pivot.distance *= sign; + // correctify the distance sign using slice polygons + float sign = (prev_layer_boundary.distance_from_lines(middle.cast()) + 0.5f * flow_width) < 0.0f ? -1.0f : 1.0f; - line_out.curled_up_height = estimate_curled_up_height(pivot, layer_region->layer()->height, flow_width, - prev_layer_curling, params); + line_out.curled_up_height = estimate_curled_up_height(middle_distance * sign, 0.5 * (a.curvature + b.curvature), + l->height, flow_width, bottom_line.curled_up_height, params); current_layer_lines.push_back(line_out); } From 3e3ccc200e703fa6b21a610a8c828be23b301873 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 13 Apr 2023 10:42:17 +0200 Subject: [PATCH 054/115] Fixed artefacts, made the new slowdown enabled with dynamic overhang speed at all times --- src/libslic3r/GCode/ExtrusionProcessor.hpp | 35 ++++++++++------------ src/libslic3r/PrintObject.cpp | 4 ++- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index d79fee83a..c80210563 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -265,30 +265,27 @@ public: std::vector extended_points = estimate_points_properties(path.polyline.points, prev_layer_boundaries[current_object], path.width); - for (size_t i = 0; i < int(extended_points.size()) - 1; i++) { - ExtendedPoint& ep = extended_points[i]; + std::vector processed_points; + processed_points.reserve(extended_points.size()); + for (size_t i = 0; i < extended_points.size(); i++) { + ExtendedPoint &curr = extended_points[i]; + const ExtendedPoint &next = extended_points[i + 1 < extended_points.size() ? i + 1 : i]; + // We are going to enforce slowdown over curled extrusions by increasing the point distance. The overhang speed is based on // signed distance from the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width // and more means full overhang, thus full slowdown. However, for curling, we take unsinged distance from the curled lines and // artifically modifiy the distance - Vec2d middle = 0.5 * (ep.position + extended_points[i + 1].position); - auto [distance_from_curled, line_idx, - p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(middle)); - if (distance_from_curled < scale_(2.0 * path.width)) { - float artificially_increased_distance = path.width * - (1.0 - (unscaled(distance_from_curled) / (2.0 * path.width)) * - (unscaled(distance_from_curled) / (2.0 * path.width))) * - (prev_curled_extrusions[current_object].get_line(line_idx).curled_height / - (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator - ep.distance = std::max(ep.distance, artificially_increased_distance); + { + Vec2d middle = 0.5 * (curr.position + next.position); + auto [distance_from_curled, line_idx, + p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(middle)); + if (distance_from_curled < scale_(2.5 * path.width)) { + float artificially_increased_distance = path.width * (1.0 - (unscaled(distance_from_curled) / (2.5 * path.width))) * + (prev_curled_extrusions[current_object].get_line(line_idx).curled_height / + (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator + curr.distance = std::max(curr.distance, artificially_increased_distance); + } } - } - - std::vector processed_points; - processed_points.reserve(extended_points.size()); - for (size_t i = 0; i < extended_points.size(); i++) { - const ExtendedPoint &curr = extended_points[i]; - const ExtendedPoint &next = extended_points[i + 1 < extended_points.size() ? i + 1 : i]; auto interpolate_speed = [](const std::map &values, float distance) { auto upper_dist = values.lower_bound(distance); diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0c3bf87b9..ed3813139 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -491,7 +491,9 @@ void PrintObject::generate_support_material() void PrintObject::estimate_curled_extrusions() { if (this->set_started(posEstimateCurledExtrusions)) { - if (this->print()->config().avoid_crossing_curled_overhangs) { + if (this->print()->config().avoid_crossing_curled_overhangs || + std::any_of(this->print()->m_print_regions.begin(), this->print()->m_print_regions.end(), + [](const PrintRegion *region) { return region->config().enable_dynamic_overhang_speeds.getBool(); })) { BOOST_LOG_TRIVIAL(debug) << "Estimating areas with curled extrusions - start"; m_print->set_status(88, _u8L("Estimating curled extrusions")); From f6c38fb7f9c836608ee77c25b010dd2c9ef79cc2 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 13 Apr 2023 14:03:50 +0200 Subject: [PATCH 055/115] Smoothen the results, fix some issues. --- src/libslic3r/AABBTreeLines.hpp | 6 ++-- src/libslic3r/GCode/ExtrusionProcessor.hpp | 39 ++++++++++++---------- src/libslic3r/SupportSpotsGenerator.cpp | 2 +- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/libslic3r/AABBTreeLines.hpp b/src/libslic3r/AABBTreeLines.hpp index ae5569b51..990b3197f 100644 --- a/src/libslic3r/AABBTreeLines.hpp +++ b/src/libslic3r/AABBTreeLines.hpp @@ -339,15 +339,15 @@ public: return {distance, nearest_line_index_out, nearest_point_out}; } - template Floating distance_from_lines(const Vec<2, typename LineType::Scalar> &point) const + template Floating distance_from_lines(const Vec<2, Scalar> &point) const { auto [dist, idx, np] = distance_from_lines_extra(point); return dist; } - std::vector all_lines_in_radius(const Vec<2, typename LineType::Scalar> &point, Floating radius) + std::vector all_lines_in_radius(const Vec<2, Scalar> &point, Floating radius) { - return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point, radius * radius); + return AABBTreeLines::all_lines_in_radius(this->lines, this->tree, point.template cast(), radius * radius); } template std::vector, size_t>> intersections_with_line(const LineType &line) const diff --git a/src/libslic3r/GCode/ExtrusionProcessor.hpp b/src/libslic3r/GCode/ExtrusionProcessor.hpp index c80210563..668c89b07 100644 --- a/src/libslic3r/GCode/ExtrusionProcessor.hpp +++ b/src/libslic3r/GCode/ExtrusionProcessor.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -268,22 +269,22 @@ public: std::vector processed_points; processed_points.reserve(extended_points.size()); for (size_t i = 0; i < extended_points.size(); i++) { - ExtendedPoint &curr = extended_points[i]; + const ExtendedPoint &curr = extended_points[i]; const ExtendedPoint &next = extended_points[i + 1 < extended_points.size() ? i + 1 : i]; - // We are going to enforce slowdown over curled extrusions by increasing the point distance. The overhang speed is based on - // signed distance from the prev layer, where 0 means fully overlapping extrusions and thus no slowdown, while extrusion_width - // and more means full overhang, thus full slowdown. However, for curling, we take unsinged distance from the curled lines and - // artifically modifiy the distance + float artificial_distance_to_curled_lines = 0.0; { - Vec2d middle = 0.5 * (curr.position + next.position); - auto [distance_from_curled, line_idx, - p] = prev_curled_extrusions[current_object].distance_from_lines_extra(Point::new_scale(middle)); - if (distance_from_curled < scale_(2.5 * path.width)) { - float artificially_increased_distance = path.width * (1.0 - (unscaled(distance_from_curled) / (2.5 * path.width))) * - (prev_curled_extrusions[current_object].get_line(line_idx).curled_height / - (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator - curr.distance = std::max(curr.distance, artificially_increased_distance); + Vec2d middle = 0.5 * (curr.position + next.position); + auto line_indices = prev_curled_extrusions[current_object].all_lines_in_radius(Point::new_scale(middle), + scale_(10.0 * path.width)); + + for (size_t idx : line_indices) { + const CurledLine &line = prev_curled_extrusions[current_object].get_line(idx); + float distance_from_curled = unscaled(line_alg::distance_to(line, Point::new_scale(middle))); + float dist = path.width * (1.0 - (distance_from_curled / (10.0 * path.width))) * + (1.0 - (distance_from_curled / (10.0 * path.width))) * + (line.curled_height / (path.height * 10.0f)); // max_curled_height_factor from SupportSpotGenerator + artificial_distance_to_curled_lines = std::max(artificial_distance_to_curled_lines, dist); } } @@ -301,12 +302,14 @@ public: return (1.0f - t) * lower_dist->second + t * upper_dist->second; }; - float extrusion_speed = std::min(interpolate_speed(speed_sections, curr.distance), - interpolate_speed(speed_sections, next.distance)); - float fan_speed = std::min(interpolate_speed(fan_speed_sections, curr.distance), - interpolate_speed(fan_speed_sections, next.distance)); + float extrusion_speed = std::min(interpolate_speed(speed_sections, curr.distance), + interpolate_speed(speed_sections, next.distance)); + float curled_base_speed = interpolate_speed(speed_sections, artificial_distance_to_curled_lines); + float final_speed = std::min(curled_base_speed, extrusion_speed); + float fan_speed = std::min(interpolate_speed(fan_speed_sections, curr.distance), + interpolate_speed(fan_speed_sections, next.distance)); - processed_points.push_back({scaled(curr.position), extrusion_speed, int(fan_speed)}); + processed_points.push_back({scaled(curr.position), final_speed, int(fan_speed)}); } return processed_points; } diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index e9f60dabd..afc213ce7 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -212,7 +212,7 @@ float estimate_curled_up_height( float distance, float curvature, float layer_height, float flow_width, float prev_line_curled_height, Params params) { float curled_up_height = 0; - if (fabs(distance) < 2.5 * flow_width) { + if (fabs(distance) < 3.0 * flow_width) { curled_up_height = std::max(prev_line_curled_height - layer_height * 0.75f, 0.0f); } From 13c774444336b50a13ed7af6faed9f471b11f91d Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 13 Apr 2023 14:37:06 +0200 Subject: [PATCH 056/115] Disable debug data output --- src/libslic3r/SupportSpotsGenerator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index afc213ce7..e5748fad3 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -41,7 +41,7 @@ #include "Geometry/ConvexHull.hpp" // #define DETAILED_DEBUG_LOGS -#define DEBUG_FILES +// #define DEBUG_FILES #ifdef DEBUG_FILES #include From a65a68ad93c7947fd5f8818947c0e35af949b271 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 21 Apr 2023 10:09:44 +0200 Subject: [PATCH 057/115] fix missing include --- src/libslic3r/GCode/GCodeProcessor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index d0c996290..02088ee7b 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -3,6 +3,7 @@ #include "libslic3r/Print.hpp" #include "libslic3r/LocalesUtils.hpp" #include "libslic3r/format.hpp" +#include "libslic3r/I18N.hpp" #include "libslic3r/GCodeWriter.hpp" #include "GCodeProcessor.hpp" From 8a959883b45f340dbf77fb312d210dd6a3ebbad4 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Fri, 21 Apr 2023 10:50:03 +0200 Subject: [PATCH 058/115] Hide supports when Manual editing mode is active in SLA supports gizmo --- src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index 109490cc7..c78a46a5b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -123,6 +123,8 @@ void GLGizmoSlaSupports::on_render() glsafe(::glEnable(GL_BLEND)); glsafe(::glEnable(GL_DEPTH_TEST)); + show_sla_supports(!m_editing_mode); + render_volumes(); render_points(selection); From 29719a7ab9661602c266d1e6b22156245b6b992a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 21 Apr 2023 15:38:23 +0200 Subject: [PATCH 059/115] ClipperLib sometimes hangs forever on a single union / diff / offset task. Implemented optional time limit on ClipperLib execution. --- src/clipper/clipper.hpp | 3 +- src/libslic3r/ClipperUtils.cpp | 52 ++++++++++++++++++++++++++++ src/libslic3r/Point.hpp | 1 + src/libslic3r/Timer.cpp | 9 +++++ src/libslic3r/Timer.hpp | 63 +++++++++++++++++++++++++++++++++- 5 files changed, 126 insertions(+), 2 deletions(-) diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index 1d6361653..d190d09b5 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -116,7 +116,8 @@ using DoublePoint = Eigen::Matrix; template using Allocator = tbb::scalable_allocator; -using Path = std::vector>; +//using Allocator = std::allocator; +using Path = std::vector>; using Paths = std::vector>; inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;} diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index d83fe4e48..95a533718 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -3,6 +3,20 @@ #include "ShortestPath.hpp" #include "Utils.hpp" +// #define CLIPPER_UTILS_TIMING + +#ifdef CLIPPER_UTILS_TIMING + // time limit for one ClipperLib operation (union / diff / offset), in ms + #define CLIPPER_UTILS_TIME_LIMIT_DEFAULT 50 + #include + #include "Timer.hpp" + #define CLIPPER_UTILS_TIME_LIMIT_SECONDS(limit) Timing::TimeLimitAlarm time_limit_alarm(uint64_t(limit) * 1000000000l, BOOST_CURRENT_FUNCTION) + #define CLIPPER_UTILS_TIME_LIMIT_MILLIS(limit) Timing::TimeLimitAlarm time_limit_alarm(uint64_t(limit) * 1000000l, BOOST_CURRENT_FUNCTION) +#else + #define CLIPPER_UTILS_TIME_LIMIT_SECONDS(limit) do {} while(false) + #define CLIPPER_UTILS_TIME_LIMIT_MILLIS(limit) do {} while(false) +#endif // CLIPPER_UTILS_TIMING + // #define CLIPPER_UTILS_DEBUG #ifdef CLIPPER_UTILS_DEBUG @@ -260,6 +274,8 @@ bool has_duplicate_points(const ClipperLib::PolyTree &polytree) template static ClipperLib::Paths raw_offset(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::ClipperOffset co; ClipperLib::Paths out; out.reserve(paths.size()); @@ -301,6 +317,8 @@ TResult clipper_do( TClip && clip, const ClipperLib::PolyFillType fillType) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Clipper clipper; clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, true); clipper.AddPaths(std::forward(clip), ClipperLib::ptClip, true); @@ -330,6 +348,8 @@ TResult clipper_union( // fillType pftNonZero and pftPositive "should" produce the same result for "normalized with implicit union" set of polygons const ClipperLib::PolyFillType fillType = ClipperLib::pftNonZero) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Clipper clipper; clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, true); TResult retval; @@ -368,6 +388,8 @@ template<> void remove_outermost_polygon(ClipperLib::PolyT template static TResult shrink_paths(PathsProvider &&paths, float offset, ClipperLib::JoinType joinType, double miterLimit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + assert(offset > 0); TResult out; if (auto raw = raw_offset(std::forward(paths), - offset, joinType, miterLimit); ! raw.empty()) { @@ -407,6 +429,8 @@ Slic3r::Polygons offset(const Slic3r::Polylines &polylines, const float delta, C // returns number of expolygons collected (0 or 1). static int offset_expolygon_inner(const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib::Paths &out) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + // 1) Offset the outer contour. ClipperLib::Paths contours; { @@ -615,6 +639,8 @@ inline ClipperLib::PolyTree clipper_do_polytree( PathProvider2 &&clip, const ClipperLib::PolyFillType fillType) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + // Perform the operation with the output to input_subject. // This pass does not generate a PolyTree, which is a very expensive operation with the current Clipper library // if there are overapping edges. @@ -753,6 +779,8 @@ Slic3r::ExPolygons union_ex(const Slic3r::Surfaces &subject) template Polylines _clipper_pl_open(ClipperLib::ClipType clipType, PathsProvider1 &&subject, PathsProvider2 &&clip) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Clipper clipper; clipper.AddPaths(std::forward(subject), ClipperLib::ptSubject, false); clipper.AddPaths(std::forward(clip), ClipperLib::ptClip, true); @@ -938,6 +966,8 @@ Polygons union_pt_chained_outside_in(const Polygons &subject) Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths output; if (preserve_collinear) { ClipperLib::Clipper c; @@ -955,6 +985,8 @@ Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + if (! preserve_collinear) return union_ex(simplify_polygons(subject, false)); @@ -971,6 +1003,8 @@ ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear Polygons top_level_islands(const Slic3r::Polygons &polygons) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + // init Clipper ClipperLib::Clipper clipper; clipper.Clear(); @@ -994,6 +1028,8 @@ ClipperLib::Paths fix_after_outer_offset( ClipperLib::PolyFillType filltype, // = ClipperLib::pftPositive bool reverse_result) // = false { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths solution; if (! input.empty()) { ClipperLib::Clipper clipper; @@ -1012,6 +1048,8 @@ ClipperLib::Paths fix_after_inner_offset( ClipperLib::PolyFillType filltype, // = ClipperLib::pftNegative bool reverse_result) // = true { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths solution; if (! input.empty()) { ClipperLib::Clipper clipper; @@ -1032,6 +1070,8 @@ ClipperLib::Paths fix_after_inner_offset( ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::vector &deltas, double miter_limit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + assert(contour.size() == deltas.size()); #ifndef NDEBUG @@ -1172,6 +1212,8 @@ ClipperLib::Path mittered_offset_path_scaled(const Points &contour, const std::v static void variable_offset_inner_raw(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit, ClipperLib::Paths &contours, ClipperLib::Paths &holes) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + #ifndef NDEBUG // Verify that the deltas are all non positive. for (const std::vector &ds : deltas) @@ -1205,6 +1247,8 @@ static void variable_offset_inner_raw(const ExPolygon &expoly, const std::vector Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths contours, holes; variable_offset_inner_raw(expoly, deltas, miter_limit, contours, holes); @@ -1227,6 +1271,8 @@ Polygons variable_offset_inner(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths contours, holes; variable_offset_inner_raw(expoly, deltas, miter_limit, contours, holes); @@ -1253,6 +1299,8 @@ ExPolygons variable_offset_inner_ex(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit, ClipperLib::Paths &contours, ClipperLib::Paths &holes) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + #ifndef NDEBUG // Verify that the deltas are all non positive. for (const std::vector &ds : deltas) @@ -1288,6 +1336,8 @@ static void variable_offset_outer_raw(const ExPolygon &expoly, const std::vector Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths contours, holes; variable_offset_outer_raw(expoly, deltas, miter_limit, contours, holes); @@ -1309,6 +1359,8 @@ Polygons variable_offset_outer(const ExPolygon &expoly, const std::vector> &deltas, double miter_limit) { + CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); + ClipperLib::Paths contours, holes; variable_offset_outer_raw(expoly, deltas, miter_limit, contours, holes); diff --git a/src/libslic3r/Point.hpp b/src/libslic3r/Point.hpp index b482129cf..c4b821ca6 100644 --- a/src/libslic3r/Point.hpp +++ b/src/libslic3r/Point.hpp @@ -54,6 +54,7 @@ using Vec4d = Eigen::Matrix; template using PointsAllocator = tbb::scalable_allocator; +//using PointsAllocator = std::allocator; using Points = std::vector>; using PointPtrs = std::vector; using PointConstPtrs = std::vector; diff --git a/src/libslic3r/Timer.cpp b/src/libslic3r/Timer.cpp index b361427a6..91f3b0f09 100644 --- a/src/libslic3r/Timer.cpp +++ b/src/libslic3r/Timer.cpp @@ -10,3 +10,12 @@ Slic3r::Timer::~Timer() BOOST_LOG_TRIVIAL(debug) << "Timer '" << m_name << "' spend " << duration_cast(steady_clock::now() - m_start).count() << "ms"; } + + +namespace Slic3r::Timing { + +void TimeLimitAlarm::report_time_exceeded() const { + BOOST_LOG_TRIVIAL(error) << "Time limit exceeded for " << m_limit_exceeded_message << ": " << m_timer.elapsed_seconds() << "s"; +} + +} diff --git a/src/libslic3r/Timer.hpp b/src/libslic3r/Timer.hpp index b8f9736a1..f2e5dde1a 100644 --- a/src/libslic3r/Timer.hpp +++ b/src/libslic3r/Timer.hpp @@ -27,5 +27,66 @@ public: ~Timer(); }; +namespace Timing { + + // Timing code from Catch2 unit testing library + static inline uint64_t nanoseconds_since_epoch() { + return std::chrono::duration_cast(std::chrono::high_resolution_clock::now().time_since_epoch()).count(); + } + + // Timing code from Catch2 unit testing library + class Timer { + public: + void start() { + m_nanoseconds = nanoseconds_since_epoch(); + } + uint64_t elapsed_nanoseconds() const { + return nanoseconds_since_epoch() - m_nanoseconds; + } + uint64_t elapsed_microseconds() const { + return elapsed_nanoseconds() / 1000; + } + unsigned int elapsed_milliseconds() const { + return static_cast(elapsed_microseconds()/1000); + } + double elapsed_seconds() const { + return elapsed_microseconds() / 1000000.0; + } + private: + uint64_t m_nanoseconds = 0; + }; + + // Emits a Boost::log error if the life time of this timing object exceeds a limit. + class TimeLimitAlarm { + public: + TimeLimitAlarm(uint64_t time_limit_nanoseconds, std::string_view limit_exceeded_message) : + m_time_limit_nanoseconds(time_limit_nanoseconds), m_limit_exceeded_message(limit_exceeded_message) { + m_timer.start(); + } + ~TimeLimitAlarm() { + auto elapsed = m_timer.elapsed_nanoseconds(); + if (elapsed > m_time_limit_nanoseconds) + this->report_time_exceeded(); + } + static TimeLimitAlarm new_nanos(uint64_t time_limit_nanoseconds, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(time_limit_nanoseconds, limit_exceeded_message); + } + static TimeLimitAlarm new_milis(uint64_t time_limit_milis, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(uint64_t(time_limit_milis) * 1000000l, limit_exceeded_message); + } + static TimeLimitAlarm new_seconds(uint64_t time_limit_seconds, std::string_view limit_exceeded_message) { + return TimeLimitAlarm(uint64_t(time_limit_seconds) * 1000000000l, limit_exceeded_message); + } + private: + void report_time_exceeded() const; + + Timer m_timer; + uint64_t m_time_limit_nanoseconds; + std::string_view m_limit_exceeded_message; + }; + +} // namespace Catch + } // namespace Slic3r -#endif // libslic3r_Timer_hpp_ \ No newline at end of file + +#endif // libslic3r_Timer_hpp_ From 0d1522791d24dc21a96ec7c56899c5793dc3e19e Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 21 Apr 2023 15:41:00 +0200 Subject: [PATCH 060/115] Refactored Arachne to use ankerl::unordered_dense hash tables instead of std::unordered_set/map The code is now generic enough to enable experiments with various hash maps in the future. --- .../Arachne/SkeletalTrapezoidation.cpp | 27 +++++++++++-------- .../Arachne/SkeletalTrapezoidation.hpp | 12 ++++++--- .../Arachne/SkeletalTrapezoidationGraph.cpp | 9 ++++--- src/libslic3r/Arachne/WallToolPaths.cpp | 5 ++-- src/libslic3r/Arachne/WallToolPaths.hpp | 6 +++-- src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp | 6 +++-- .../Arachne/utils/PolylineStitcher.hpp | 1 - src/libslic3r/Arachne/utils/SparseGrid.hpp | 1 - .../Arachne/utils/SparseLineGrid.hpp | 1 - .../Arachne/utils/SparsePointGrid.hpp | 1 - src/libslic3r/Arachne/utils/SquareGrid.hpp | 1 - src/libslic3r/PerimeterGenerator.cpp | 14 +++++----- src/libslic3r/ShortEdgeCollapse.cpp | 4 ++- 13 files changed, 49 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index a73a4918a..26d2dbeee 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -5,7 +5,6 @@ #include #include -#include #include #include #include @@ -522,9 +521,11 @@ static bool has_missing_twin_edge(const SkeletalTrapezoidationGraph &graph) return false; } -inline static void rotate_back_skeletal_trapezoidation_graph_after_fix(SkeletalTrapezoidationGraph &graph, - const double fix_angle, - const std::unordered_map &vertex_mapping) +using PointMap = SkeletalTrapezoidation::PointMap; + +inline static void rotate_back_skeletal_trapezoidation_graph_after_fix(SkeletalTrapezoidationGraph &graph, + const double fix_angle, + const PointMap &vertex_mapping) { for (STHalfEdgeNode &node : graph.nodes) { // If a mapping exists between a rotated point and an original point, use this mapping. Otherwise, rotate a point in the opposite direction. @@ -588,7 +589,7 @@ VoronoiDiagramStatus detect_voronoi_diagram_known_issues(const Geometry::Voronoi return VoronoiDiagramStatus::NO_ISSUE_DETECTED; } -inline static std::pair, double> try_to_fix_degenerated_voronoi_diagram_by_rotation( +inline static std::pair try_to_fix_degenerated_voronoi_diagram_by_rotation( Geometry::VoronoiDiagram &voronoi_diagram, const Polygons &polys, Polygons &polys_rotated, @@ -597,7 +598,7 @@ inline static std::pair, double> try { const Polygons polys_rotated_original = polys_rotated; double fixed_by_angle = fix_angles.front(); - std::unordered_map vertex_mapping; + PointMap vertex_mapping; for (const double &fix_angle : fix_angles) { vertex_mapping.clear(); @@ -685,7 +686,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) const std::vector fix_angles = {PI / 6, PI / 5, PI / 7, PI / 11}; double fixed_by_angle = fix_angles.front(); - std::unordered_map vertex_mapping; + PointMap vertex_mapping; // polys_copy is referenced through items stored in the std::vector segments. Polygons polys_copy = polys; if (status != VoronoiDiagramStatus::NO_ISSUE_DETECTED) { @@ -813,9 +814,11 @@ process_voronoi_diagram: edge.from->incident_edge = &edge; } +using NodeSet = SkeletalTrapezoidation::NodeSet; + void SkeletalTrapezoidation::separatePointyQuadEndNodes() { - std::unordered_set visited_nodes; + NodeSet visited_nodes; for (edge_t& edge : graph.edges) { if (edge.prev) @@ -2285,16 +2288,18 @@ void SkeletalTrapezoidation::addToolpathSegment(const ExtrusionJunction& from, c void SkeletalTrapezoidation::connectJunctions(ptr_vector_t& edge_junctions) { - std::unordered_set unprocessed_quad_starts(graph.edges.size() * 5 / 2); + using EdgeSet = ankerl::unordered_dense::set; + + EdgeSet unprocessed_quad_starts(graph.edges.size() * 5 / 2); for (edge_t& edge : graph.edges) { if (!edge.prev) { - unprocessed_quad_starts.insert(&edge); + unprocessed_quad_starts.emplace(&edge); } } - std::unordered_set passed_odd_edges; + EdgeSet passed_odd_edges; while (!unprocessed_quad_starts.empty()) { diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index 7b8ecf834..e2a013b15 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -7,8 +7,10 @@ #include #include // smart pointers -#include #include // pair + +#include + #include #include "utils/HalfEdgeGraph.hpp" @@ -80,7 +82,9 @@ class SkeletalTrapezoidation const BeadingStrategy& beading_strategy; public: - using Segment = PolygonsSegmentIndex; + using Segment = PolygonsSegmentIndex; + using PointMap = ankerl::unordered_dense::map; + using NodeSet = ankerl::unordered_dense::set; /*! * Construct a new trapezoidation problem to solve. @@ -164,8 +168,8 @@ protected: * mapping each voronoi VD edge to the corresponding halfedge HE edge * In case the result segment is discretized, we map the VD edge to the *last* HE edge */ - std::unordered_map vd_edge_to_he_edge; - std::unordered_map vd_node_to_he_node; + ankerl::unordered_dense::map vd_edge_to_he_edge; + ankerl::unordered_dense::map vd_node_to_he_node; node_t& makeNode(vd_t::vertex_type& vd_node, Point p); //!< Get the node which the VD node maps to, or create a new mapping if there wasn't any yet. /*! diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp index 4ef96eda1..4629396e8 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp @@ -2,7 +2,8 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include "SkeletalTrapezoidationGraph.hpp" -#include + +#include #include @@ -180,8 +181,8 @@ bool STHalfEdgeNode::isLocalMaximum(bool strict) const void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) { - std::unordered_map::iterator> edge_locator; - std::unordered_map::iterator> node_locator; + ankerl::unordered_dense::map edge_locator; + ankerl::unordered_dense::map node_locator; for (auto edge_it = edges.begin(); edge_it != edges.end(); ++edge_it) { @@ -193,7 +194,7 @@ void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) node_locator.emplace(&*node_it, node_it); } - auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, std::list::iterator& current_edge_it, bool& edge_it_is_updated) + auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, Edges::iterator& current_edge_it, bool& edge_it_is_updated) { if (current_edge_it != edges.end() && to_be_removed == &*current_edge_it) diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index 1c69fa9ac..fce69d5e4 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -2,7 +2,6 @@ // CuraEngine is released under the terms of the AGPLv3 or higher. #include //For std::partition_copy and std::min_element. -#include #include "WallToolPaths.hpp" @@ -767,9 +766,9 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector &toolpa * * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. */ -std::unordered_set, boost::hash>> WallToolPaths::getRegionOrder(const std::vector &input, const bool outer_to_inner) +WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector &input, const bool outer_to_inner) { - std::unordered_set, boost::hash>> order_requirements; + ExtrusionLineSet order_requirements; // We build a grid where we map toolpath vertex locations to toolpaths, // so that we can easily find which two toolpaths are next to each other, diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp index b0bed1241..44f3affb6 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -5,7 +5,8 @@ #define CURAENGINE_WALLTOOLPATHS_H #include -#include + +#include #include "BeadingStrategy/BeadingStrategyFactory.hpp" #include "utils/ExtrusionLine.hpp" @@ -73,6 +74,7 @@ public: */ static bool removeEmptyToolPaths(std::vector &toolpaths); + using ExtrusionLineSet = ankerl::unordered_dense::set, boost::hash>>; /*! * Get the order constraints of the insets when printing walls per region / hole. * Each returned pair consists of adjacent wall lines where the left has an inset_idx one lower than the right. @@ -81,7 +83,7 @@ public: * * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. */ - static std::unordered_set, boost::hash>> getRegionOrder(const std::vector &input, bool outer_to_inner); + static ExtrusionLineSet getRegionOrder(const std::vector &input, bool outer_to_inner); protected: /*! diff --git a/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp index 99efff6a0..17b06f2be 100644 --- a/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp +++ b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp @@ -21,8 +21,10 @@ class HalfEdgeGraph public: using edge_t = derived_edge_t; using node_t = derived_node_t; - std::list edges; - std::list nodes; + using Edges = std::list; + using Nodes = std::list; + Edges edges; + Nodes nodes; }; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/PolylineStitcher.hpp b/src/libslic3r/Arachne/utils/PolylineStitcher.hpp index 2ab770a3e..113761ce1 100644 --- a/src/libslic3r/Arachne/utils/PolylineStitcher.hpp +++ b/src/libslic3r/Arachne/utils/PolylineStitcher.hpp @@ -7,7 +7,6 @@ #include "SparsePointGrid.hpp" #include "PolygonsPointIndex.hpp" #include "../../Polygon.hpp" -#include #include namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/SparseGrid.hpp b/src/libslic3r/Arachne/utils/SparseGrid.hpp index be461d424..45876fb9a 100644 --- a/src/libslic3r/Arachne/utils/SparseGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparseGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_GRID_H #include -#include #include #include diff --git a/src/libslic3r/Arachne/utils/SparseLineGrid.hpp b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp index a9b536869..0b38988f9 100644 --- a/src/libslic3r/Arachne/utils/SparseLineGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_LINE_GRID_H #include -#include #include #include diff --git a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp index 31c196535..7bb51d703 100644 --- a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_POINT_GRID_H #include -#include #include #include "SparseGrid.hpp" diff --git a/src/libslic3r/Arachne/utils/SquareGrid.hpp b/src/libslic3r/Arachne/utils/SquareGrid.hpp index c59c3ee1b..5787e3bf1 100644 --- a/src/libslic3r/Arachne/utils/SquareGrid.hpp +++ b/src/libslic3r/Arachne/utils/SquareGrid.hpp @@ -7,7 +7,6 @@ #include "../../Point.hpp" #include -#include #include #include diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 89add5978..1a487cba8 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -39,11 +39,11 @@ #include #include #include -#include -#include #include #include +#include + // #define ARACHNE_DEBUG #ifdef ARACHNE_DEBUG @@ -569,7 +569,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator::P size_t occurrence = 0; bool is_overhang = false; }; - std::unordered_map point_occurrence; + ankerl::unordered_dense::map point_occurrence; for (const ExtrusionPath &path : paths) { ++point_occurrence[path.polyline.first_point()].occurrence; ++point_occurrence[path.polyline.last_point()].occurrence; @@ -681,7 +681,7 @@ Polylines reconnect_polylines(const Polylines &polylines, double limit_distance) if (polylines.empty()) return polylines; - std::unordered_map connected; + ankerl::unordered_dense::map connected; connected.reserve(polylines.size()); for (size_t i = 0; i < polylines.size(); i++) { if (!polylines[i].empty()) { @@ -731,7 +731,7 @@ ExtrusionPaths sort_extra_perimeters(ExtrusionPaths extra_perims, int index_of_f { if (extra_perims.empty()) return {}; - std::vector> dependencies(extra_perims.size()); + std::vector> dependencies(extra_perims.size()); for (size_t path_idx = 0; path_idx < extra_perims.size(); path_idx++) { for (size_t prev_path_idx = 0; prev_path_idx < path_idx; prev_path_idx++) { if (paths_touch(extra_perims[path_idx], extra_perims[prev_path_idx], extrusion_spacing * 1.5f)) { @@ -1153,11 +1153,11 @@ void PerimeterGenerator::process_arachne( // Find topological order with constraints from extrusions_constrains. std::vector blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion. std::vector> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion. - std::unordered_map map_extrusion_to_idx; + ankerl::unordered_dense::map map_extrusion_to_idx; for (size_t idx = 0; idx < all_extrusions.size(); idx++) map_extrusion_to_idx.emplace(all_extrusions[idx], idx); - auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, params.config.external_perimeters_first); + Arachne::WallToolPaths::ExtrusionLineSet extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, params.config.external_perimeters_first); for (auto [before, after] : extrusions_constrains) { auto after_it = map_extrusion_to_idx.find(after); ++blocked[after_it->second]; diff --git a/src/libslic3r/ShortEdgeCollapse.cpp b/src/libslic3r/ShortEdgeCollapse.cpp index 0c940cb47..c8e4eb97e 100644 --- a/src/libslic3r/ShortEdgeCollapse.cpp +++ b/src/libslic3r/ShortEdgeCollapse.cpp @@ -6,6 +6,8 @@ #include #include +#include + namespace Slic3r { void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_count) { @@ -155,7 +157,7 @@ void its_short_edge_collpase(indexed_triangle_set &mesh, size_t target_triangle_ } //Extract the result mesh - std::unordered_map final_vertices_mapping; + ankerl::unordered_dense::map final_vertices_mapping; std::vector final_vertices; std::vector final_indices; final_indices.reserve(face_indices.size()); From b37d4e5b6c7b3f2847a18300a95c64bf1e21a989 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 24 Apr 2023 10:01:32 +0200 Subject: [PATCH 061/115] SPE-1674 - Follow-up of 9efed4be225615b3fac420bc2993cd0cb1e3429c - More robust fix --- src/slic3r/GUI/GLCanvas3D.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 23079b63c..bc0fdf65c 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -615,7 +615,7 @@ void GLCanvas3D::LayersEditing::accept_changes(GLCanvas3D& canvas) if (m_layer_height_profile_modified) { wxGetApp().plater()->take_snapshot(_L("Variable layer height - Manual edit")); const_cast(m_model_object)->layer_height_profile.set(m_layer_height_profile); - canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); + canvas.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); wxGetApp().obj_list()->update_info_items(last_object_id); } } @@ -3189,9 +3189,15 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt) m_canvas->SetFocus(); if (evt.Entering()) { - if (m_mouse.dragging && !evt.LeftIsDown() && !evt.RightIsDown() && !evt.MiddleIsDown()) - // reset dragging state if the user released the mouse button outside the 3D scene - m_mouse.dragging = false; + if (m_mouse.dragging && !evt.LeftIsDown() && !evt.RightIsDown() && !evt.MiddleIsDown()) { + // ensure to stop layers editing if enabled + if (m_layers_editing.state != LayersEditing::Unknown) { + m_layers_editing.state = LayersEditing::Unknown; + _stop_timer(); + m_layers_editing.accept_changes(*this); + } + mouse_up_cleanup(); + } //#if defined(__WXMSW__) || defined(__linux__) // // On Windows and Linux needs focus in order to catch key events From 003350a4e2a4677f95ad7a7a9c20ed2f669eead0 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Mon, 24 Apr 2023 11:22:43 +0200 Subject: [PATCH 062/115] Fix of version notification message. --- src/slic3r/GUI/GUI_App.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index dc4bb17e8..b0411c89c 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -3381,6 +3381,9 @@ void GUI_App::on_version_read(wxCommandEvent& evt) ? _u8L("Check for application update has failed.") : Slic3r::format(_u8L("You are currently running the latest released version %1%."), evt.GetString()); + if (*Semver::parse(SLIC3R_VERSION) > *Semver::parse(into_u8(evt.GetString()))) + text = Slic3r::format(_u8L("There are no new released versions online. The latest release version is %1%."), evt.GetString()); + this->plater_->get_notification_manager()->push_version_notification(NotificationType::NoNewReleaseAvailable , NotificationManager::NotificationLevel::RegularNotificationLevel , text From 9de269889cd80a66af7a4c2ddc25451f5f58b083 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 24 Apr 2023 11:34:24 +0200 Subject: [PATCH 063/115] Improved parallel_foor grain size for ensuring --- src/libslic3r/PrintObject.cpp | 53 ++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index c6329f2e4..46588b9b0 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -56,6 +56,20 @@ using namespace std::literals; +// #define PRINT_OBJECT_TIMING + +#ifdef PRINT_OBJECT_TIMING + // time limit for one ClipperLib operation (union / diff / offset), in ms + #define PRINT_OBJECT_TIME_LIMIT_DEFAULT 50 + #include + #include "Timer.hpp" + #define PRINT_OBJECT_TIME_LIMIT_SECONDS(limit) Timing::TimeLimitAlarm time_limit_alarm(uint64_t(limit) * 1000000000l, BOOST_CURRENT_FUNCTION) + #define PRINT_OBJECT_TIME_LIMIT_MILLIS(limit) Timing::TimeLimitAlarm time_limit_alarm(uint64_t(limit) * 1000000l, BOOST_CURRENT_FUNCTION) +#else + #define PRINT_OBJECT_TIME_LIMIT_SECONDS(limit) do {} while(false) + #define PRINT_OBJECT_TIME_LIMIT_MILLIS(limit) do {} while(false) +#endif // PRINT_OBJECT_TIMING + #ifdef SLIC3R_DEBUG_SLICE_PROCESSING #define SLIC3R_DEBUG #endif @@ -178,6 +192,7 @@ void PrintObject::make_perimeters() tbb::parallel_for( tbb::blocked_range(0, m_layers.size() - 1), [this, ®ion, region_id](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); LayerRegion &layerm = *m_layers[layer_idx]->get_region(region_id); @@ -237,6 +252,7 @@ void PrintObject::make_perimeters() tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); m_layers[layer_idx]->make_perimeters(); @@ -408,6 +424,7 @@ void PrintObject::infill() tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get(), this->m_lightning_generator.get()); @@ -431,6 +448,7 @@ void PrintObject::ironing() // Ironing starting with layer 0 to support ironing all surfaces. tbb::blocked_range(0, m_layers.size()), [this](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); m_layers[layer_idx]->make_ironing(); @@ -530,16 +548,17 @@ std::pair PrintObject::prepare std::vector> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1))); // ^ make sure vector is not empty, even with no briding surfaces we still want to build the adaptive trees later, some continue normally tbb::parallel_for(tbb::blocked_range(0, surfaces_w_bottom_z.size()), - [this, &to_octree, &overhangs, &surfaces_w_bottom_z](const tbb::blocked_range &range) { - for (int surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) { - std::vector &out = overhangs[surface_idx]; - m_print->throw_if_canceled(); - append(out, triangulate_expolygon_3d(surfaces_w_bottom_z[surface_idx].first->expolygon, - surfaces_w_bottom_z[surface_idx].second)); - for (Vec3d &p : out) - p = (to_octree * p).eval(); - } - }); + [this, &to_octree, &overhangs, &surfaces_w_bottom_z](const tbb::blocked_range &range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); + for (int surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) { + std::vector &out = overhangs[surface_idx]; + m_print->throw_if_canceled(); + append(out, triangulate_expolygon_3d(surfaces_w_bottom_z[surface_idx].first->expolygon, + surfaces_w_bottom_z[surface_idx].second)); + for (Vec3d &p : out) + p = (to_octree * p).eval(); + } + }); // and gather them. for (size_t i = 1; i < overhangs.size(); ++ i) append(overhangs.front(), std::move(overhangs[i])); @@ -911,6 +930,7 @@ void PrintObject::detect_surfaces_type() // In non-spiral vase mode, go over all layers. m_layers.size()), [this, region_id, interface_shells, &surfaces_new](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); // If we have soluble support material, don't bridge. The overhang will be squished against a soluble layer separating // the support from the print. SurfaceType surface_type_bottom_other = @@ -1059,6 +1079,7 @@ void PrintObject::detect_surfaces_type() tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this, region_id](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { m_print->throw_if_canceled(); LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id]; @@ -1117,6 +1138,7 @@ void PrintObject::process_external_surfaces() tbb::parallel_for( tbb::blocked_range(0, m_layers.size() - 1), [this, &surfaces_covered, &layer_expansions_and_voids, unsupported_width](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) if (layer_expansions_and_voids[layer_idx + 1]) { // Layer above is partially filled with solid infill (top, bottom, bridging...), @@ -1142,6 +1164,7 @@ void PrintObject::process_external_surfaces() tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), [this, &surfaces_covered, region_id](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); // BOOST_LOG_TRIVIAL(trace) << "Processing external surface, layer" << m_layers[layer_idx]->print_z; @@ -1194,6 +1217,7 @@ void PrintObject::discover_vertical_shells() tbb::parallel_for( tbb::blocked_range(0, num_layers, grain_size), [this, &cache_top_botom_regions](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); const std::initializer_list surfaces_bottom { stBottom, stBottomBridge }; const size_t num_regions = this->num_printing_regions(); for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { @@ -1267,6 +1291,7 @@ void PrintObject::discover_vertical_shells() tbb::parallel_for( tbb::blocked_range(0, num_layers, grain_size), [this, region_id, &cache_top_botom_regions](const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); const std::initializer_list surfaces_bottom { stBottom, stBottomBridge }; for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { m_print->throw_if_canceled(); @@ -1292,10 +1317,12 @@ void PrintObject::discover_vertical_shells() } BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells for region " << region_id << " in parallel - start : ensure vertical wall thickness"; + grain_size = 1; tbb::parallel_for( tbb::blocked_range(0, num_layers, grain_size), [this, region_id, &cache_top_botom_regions] (const tbb::blocked_range& range) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); // printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end()); for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) { m_print->throw_if_canceled(); @@ -1626,6 +1653,7 @@ void PrintObject::bridge_over_infill() tbb::concurrent_vector candidate_surfaces; tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = static_cast(this), &candidate_surfaces](tbb::blocked_range r) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t lidx = r.begin(); lidx < r.end(); lidx++) { const Layer *layer = po->get_layer(lidx); if (layer->lower_layer == nullptr) { @@ -1723,6 +1751,7 @@ void PrintObject::bridge_over_infill() tbb::parallel_for(tbb::blocked_range(0, layers_to_generate_infill.size()), [po = static_cast(this), &layers_to_generate_infill, &infill_lines](tbb::blocked_range r) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) { size_t lidx = layers_to_generate_infill[job_idx]; infill_lines.at( @@ -1754,6 +1783,7 @@ void PrintObject::bridge_over_infill() tbb::parallel_for(tbb::blocked_range(0, layers_with_candidates.size()), [&layers_with_candidates, &surfaces_by_layer, &layer_area_covered_by_candidates]( tbb::blocked_range r) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) { size_t lidx = layers_with_candidates[job_idx]; for (const auto &candidate : surfaces_by_layer.at(lidx)) { @@ -2072,6 +2102,7 @@ void PrintObject::bridge_over_infill() determine_bridging_angle, construct_anchored_polygon]( tbb::blocked_range r) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t cluster_idx = r.begin(); cluster_idx < r.end(); cluster_idx++) { for (size_t job_idx = 0; job_idx < clustered_layers_for_threads[cluster_idx].size(); job_idx++) { size_t lidx = clustered_layers_for_threads[cluster_idx][job_idx]; @@ -2244,6 +2275,7 @@ void PrintObject::bridge_over_infill() BOOST_LOG_TRIVIAL(info) << "Bridge over infill - Directions and expanded surfaces computed" << log_memory_info(); tbb::parallel_for(tbb::blocked_range(0, this->layers().size()), [po = this, &surfaces_by_layer](tbb::blocked_range r) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (size_t lidx = r.begin(); lidx < r.end(); lidx++) { if (surfaces_by_layer.find(lidx) == surfaces_by_layer.end()) continue; @@ -2760,6 +2792,7 @@ static void project_triangles_to_slabs(SpanOfConstPtrs layers, const inde [&custom_facets, &tr, tr_det_sign, seam, layers, &projections_of_triangles](const tbb::blocked_range& range) { for (size_t idx = range.begin(); idx < range.end(); ++ idx) { + PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); std::array facet; // Transform the triangle into worlds coords. From bbea397aa662a01e8b42ea13159aa4109a615573 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 25 Apr 2023 10:02:50 +0200 Subject: [PATCH 064/115] SPE-1675 - Do not update layer editing when the mouse cursor is moved outside of the 3D scene while dragging --- src/slic3r/GUI/GLCanvas3D.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bc0fdf65c..943f08a63 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -6244,10 +6244,10 @@ void GLCanvas3D::_perform_layer_editing_action(wxMouseEvent* evt) m_layers_editing.last_action = evt->ShiftDown() ? (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_SMOOTH : LAYER_HEIGHT_EDIT_ACTION_REDUCE) : (evt->RightIsDown() ? LAYER_HEIGHT_EDIT_ACTION_INCREASE : LAYER_HEIGHT_EDIT_ACTION_DECREASE); - } - m_layers_editing.adjust_layer_height_profile(); - _refresh_if_shown_on_screen(); + m_layers_editing.adjust_layer_height_profile(); + _refresh_if_shown_on_screen(); + } // Automatic action on mouse down with the same coordinate. _start_timer(); From f9c1abbd50635dadb0b423bbc07418a85ea471d7 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Mon, 24 Apr 2023 13:58:01 +0200 Subject: [PATCH 065/115] SPE-1677 - Disable SLA supports and Hollow gizmos when the selected object is non-printable. Do not allow to set to non-printable an object while the SLA supports and Hollow gizmos are active. --- src/slic3r/GUI/GUI_Factories.cpp | 8 ++++++++ src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp | 6 ++++++ src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp | 14 ++++++++++++-- src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp | 7 ++++--- 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp index 4c12fbe23..045bd146a 100644 --- a/src/slic3r/GUI/GUI_Factories.cpp +++ b/src/slic3r/GUI/GUI_Factories.cpp @@ -743,6 +743,14 @@ wxMenuItem* MenuFactory::append_menu_item_printable(wxMenu* menu) } evt.Check(check); + + // disable the menu item if SLA supports or Hollow gizmos are active + if (printer_technology() == ptSLA) { + const auto gizmo_type = plater()->canvas3D()->get_gizmos_manager().get_current_type(); + const bool enable = gizmo_type != GLGizmosManager::SlaSupports && gizmo_type != GLGizmosManager::Hollow; + evt.Enable(enable); + } + plater()->set_current_canvas_as_dirty(); }, menu_item_printable->GetId()); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp index 91b2bd879..0a4217387 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoHollow.cpp @@ -820,6 +820,12 @@ bool GLGizmoHollow::on_is_activable() const if (selection.get_volume(idx)->is_outside && selection.get_volume(idx)->composite_id.volume_id >= 0) return false; + // Check that none of the selected volumes is marked as non-pritable. + for (const auto& idx : list) { + if (!selection.get_volume(idx)->printable) + return false; + } + return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp index c78a46a5b..b9ec5cf19 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSlaSupports.cpp @@ -65,9 +65,13 @@ void GLGizmoSlaSupports::data_changed(bool is_serializing) // If we triggered autogeneration before, check backend and fetch results if they are there if (mo) { m_c->instances_hider()->set_hide_full_scene(true); - const SLAPrintObject* po = m_c->selection_info()->print_object(); + + int last_comp_step = slaposCount; const int required_step = get_min_sla_print_object_step(); - auto last_comp_step = static_cast(po->last_completed_step()); + const SLAPrintObject* po = m_c->selection_info()->print_object(); + if (po != nullptr) + last_comp_step = static_cast(po->last_completed_step()); + if (last_comp_step == slaposCount) last_comp_step = -1; @@ -793,6 +797,12 @@ bool GLGizmoSlaSupports::on_is_activable() const if (selection.get_volume(idx)->is_outside && selection.get_volume(idx)->composite_id.volume_id >= 0) return false; + // Check that none of the selected volumes is marked as non-pritable. + for (const auto& idx : list) { + if (!selection.get_volume(idx)->printable) + return false; + } + return true; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp index debb22535..4edb01c2b 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmosCommon.cpp @@ -261,11 +261,12 @@ void Raycaster::on_update() // For sla printers we use the mesh generated by the backend std::shared_ptr preview_mesh_ptr; const SLAPrintObject* po = get_pool()->selection_info()->print_object(); - if (po) + if (po != nullptr) preview_mesh_ptr = po->get_mesh_to_print(); + else + preview_mesh_ptr.reset(); - if (preview_mesh_ptr) - m_sla_mesh_cache = TriangleMesh{*preview_mesh_ptr}; + m_sla_mesh_cache = (preview_mesh_ptr != nullptr) ? TriangleMesh{ *preview_mesh_ptr } : TriangleMesh(); if (!m_sla_mesh_cache.empty()) { m_sla_mesh_cache.transform(po->trafo().inverse()); From f1c5ffddfa780b868573ff803a79811e431a6def Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 26 Apr 2023 09:06:17 +0200 Subject: [PATCH 066/115] Fixed typo --- src/slic3r/GUI/GLCanvas3D.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 943f08a63..dbb210680 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -6274,7 +6274,7 @@ Vec3d GLCanvas3D::_mouse_to_3d(const Point& mouse_pos, float* z) Vec3d GLCanvas3D::_mouse_to_bed_3d(const Point& mouse_pos) { const Linef3 ray = mouse_ray(mouse_pos); - return (std::abs(ray.unit_vector().z() < EPSILON)) ? ray.a : ray.intersect_plane(0.0); + return (std::abs(ray.unit_vector().z()) < EPSILON) ? ray.a : ray.intersect_plane(0.0); } void GLCanvas3D::_start_timer() From 4986afe94fc725e07b2410022d82486b7d3f8720 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 26 Apr 2023 09:34:02 +0200 Subject: [PATCH 067/115] Tech ENABLE_GCODE_POSTPROCESS_BACKTRACE set as default --- src/libslic3r/GCode/GCodeProcessor.cpp | 144 ------------------------- src/libslic3r/GCode/GCodeProcessor.hpp | 2 - src/libslic3r/GCodeReader.hpp | 2 - src/libslic3r/Technologies.hpp | 2 - 4 files changed, 150 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index 02088ee7b..9dab740e5 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -442,9 +442,7 @@ void GCodeProcessorResult::reset() { max_print_height = 0.0f; settings_ids.reset(); extruders_count = 0; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE backtrace_enabled = false; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE extruder_colors = std::vector(); filament_diameters = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DIAMETER); filament_densities = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DENSITY); @@ -462,9 +460,7 @@ void GCodeProcessorResult::reset() { max_print_height = 0.0f; settings_ids.reset(); extruders_count = 0; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE backtrace_enabled = false; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE extruder_colors = std::vector(); filament_diameters = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DIAMETER); filament_densities = std::vector(MIN_EXTRUDERS_COUNT, DEFAULT_FILAMENT_DENSITY); @@ -558,9 +554,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config) m_producer = EProducer::PrusaSlicer; m_flavor = config.gcode_flavor; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE m_result.backtrace_enabled = is_XL_printer(config); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE size_t extruders_count = config.nozzle_diameter.values.size(); m_result.extruders_count = extruders_count; @@ -3477,7 +3471,6 @@ void GCodeProcessor::post_process() last_exported_stop[i] = time_in_minutes(m_time_processor.machines[i].time); } -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE // Helper class to modify and export gcode to file class ExportLines { @@ -3712,26 +3705,15 @@ void GCodeProcessor::post_process() }; ExportLines export_lines(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, m_time_processor.machines[0]); -#else - // buffer line to export only when greater than 64K to reduce writing calls - std::string export_line; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE // replace placeholder lines with the proper final value // gcode_line is in/out parameter, to reduce expensive memory allocation auto process_placeholders = [&](std::string& gcode_line) { -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE bool processed = false; -#else - unsigned int extra_lines_count = 0; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE // remove trailing '\n' auto line = std::string_view(gcode_line).substr(0, gcode_line.length() - 1); -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - std::string ret; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE if (line.length() > 1) { line = line.substr(1); if (m_time_processor.export_remaining_time_enabled && @@ -3740,29 +3722,16 @@ void GCodeProcessor::post_process() const TimeMachine& machine = m_time_processor.machines[i]; if (machine.enabled) { // export pair -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? 0 : 100, (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? time_in_minutes(machine.time) : 0)); processed = true; -#else - ret += format_line_M73_main(machine.line_m73_main_mask.c_str(), - (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? 0 : 100, - (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? time_in_minutes(machine.time) : 0); - ++extra_lines_count; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE // export remaining time to next printer stop if (line == reserved_tag(ETags::First_Line_M73_Placeholder) && !machine.stop_times.empty()) { const int to_export_stop = time_in_minutes(machine.stop_times.front().elapsed_time); -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop)); last_exported_stop[i] = to_export_stop; -#else - ret += format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop); - last_exported_stop[i] = to_export_stop; - ++extra_lines_count; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE } } } @@ -3776,12 +3745,8 @@ void GCodeProcessor::post_process() sprintf(buf, "; estimated printing time (%s mode) = %s\n", (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", get_time_dhms(machine.time).c_str()); -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(buf); processed = true; -#else - ret += buf; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE } } for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { @@ -3792,25 +3757,14 @@ void GCodeProcessor::post_process() sprintf(buf, "; estimated first layer printing time (%s mode) = %s\n", (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", get_time_dhms(machine.layers_time.empty() ? 0.f : machine.layers_time.front()).c_str()); -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(buf); processed = true; -#else - ret += buf; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE } } } } -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE return processed; -#else - if (!ret.empty()) - // Not moving the move operator on purpose, so that the gcode_line allocation will grow and it will not be reallocated after handful of lines are processed. - gcode_line = ret; - return std::tuple(!ret.empty(), (extra_lines_count == 0) ? extra_lines_count : extra_lines_count - 1); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE }; std::vector filament_mm(m_result.extruders_count, 0.0); @@ -3884,16 +3838,8 @@ void GCodeProcessor::post_process() time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute, // Caches, to be modified &g1_times_cache_it, &last_exported_main, &last_exported_stop, -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE &export_lines] -#else - // String output - &export_line] -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE (const size_t g1_lines_counter) { -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - unsigned int exported_lines_count = 0; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE if (m_time_processor.export_remaining_time_enabled) { for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { const TimeMachine& machine = m_time_processor.machines[i]; @@ -3907,17 +3853,9 @@ void GCodeProcessor::post_process() std::pair to_export_main = { int(100.0f * it->elapsed_time / machine.time), time_in_minutes(machine.time - it->elapsed_time) }; if (last_exported_main[i] != to_export_main) { -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), to_export_main.first, to_export_main.second)); -#else - export_line += format_line_M73_main(machine.line_m73_main_mask.c_str(), - to_export_main.first, to_export_main.second); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE last_exported_main[i] = to_export_main; -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - ++exported_lines_count; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE } // export remaining time to next printer stop auto it_stop = std::upper_bound(machine.stop_times.begin(), machine.stop_times.end(), it->elapsed_time, @@ -3927,15 +3865,8 @@ void GCodeProcessor::post_process() if (last_exported_stop[i] != to_export_stop) { if (to_export_stop > 0) { if (last_exported_stop[i] != to_export_stop) { -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop)); -#else - export_line += format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE last_exported_stop[i] = to_export_stop; -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - ++exported_lines_count; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE } } else { @@ -3954,22 +3885,12 @@ void GCodeProcessor::post_process() } if (is_last) { -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE if (std::distance(machine.stop_times.begin(), it_stop) == static_cast(machine.stop_times.size() - 1)) export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop)); else export_lines.append_line(format_line_M73_stop_float(machine.line_m73_stop_mask.c_str(), time_in_last_minute(it_stop->elapsed_time - it->elapsed_time))); -#else - if (std::distance(machine.stop_times.begin(), it_stop) == static_cast(machine.stop_times.size() - 1)) - export_line += format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop); - else - export_line += format_line_M73_stop_float(machine.line_m73_stop_mask.c_str(), time_in_last_minute(it_stop->elapsed_time - it->elapsed_time)); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE last_exported_stop[i] = to_export_stop; -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - ++exported_lines_count; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE } } } @@ -3978,12 +3899,8 @@ void GCodeProcessor::post_process() } } } -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - return exported_lines_count; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE }; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE // add lines XXX to exported gcode auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); @@ -4028,37 +3945,15 @@ void GCodeProcessor::post_process() }); } }; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE m_result.lines_ends.clear(); -#if !ENABLE_GCODE_POSTPROCESS_BACKTRACE - // helper function to write to disk - size_t out_file_pos = 0; - auto write_string = [this, &export_line, &out, &out_path, &out_file_pos](const std::string& str) { - fwrite((const void*)export_line.c_str(), 1, export_line.length(), out.f); - if (ferror(out.f)) { - out.close(); - boost::nowide::remove(out_path.c_str()); - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nIs the disk full?\n")); - } - for (size_t i = 0; i < export_line.size(); ++i) - if (export_line[i] == '\n') - m_result.lines_ends.emplace_back(out_file_pos + i + 1); - out_file_pos += export_line.size(); - export_line.clear(); - }; -#endif // !ENABLE_GCODE_POSTPROCESS_BACKTRACE unsigned int line_id = 0; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE // Backtrace data for Tx gcode lines static const ExportLines::Backtrace backtrace_T = { 120.0f, 10 }; // In case there are multiple sources of backtracing, keeps track of the longest backtrack time needed // to flush the backtrace cache accordingly float max_backtrace_time = 120.0f; -#else - std::vector> offsets; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE { // Read the input stream 64kB at a time, extract lines and process them. @@ -4082,24 +3977,15 @@ void GCodeProcessor::post_process() gcode_line.insert(gcode_line.end(), it, it_end); if (eol) { ++line_id; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.update(line_id, g1_lines_counter); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE gcode_line += "\n"; // replace placeholder lines -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE bool processed = process_placeholders(gcode_line); if (processed) gcode_line.clear(); -#else - auto [processed, lines_added_count] = process_placeholders(gcode_line); - if (processed && lines_added_count > 0) - offsets.push_back({ line_id, lines_added_count }); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE if (!processed) processed = process_used_filament(gcode_line); -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE if (!processed && !is_temporary_decoration(gcode_line)) { if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) // add lines M73 where needed @@ -4114,18 +4000,6 @@ void GCodeProcessor::post_process() if (!gcode_line.empty()) export_lines.append_line(gcode_line); export_lines.write(out, 1.1f * max_backtrace_time, m_result, out_path); -#else - if (!processed && !is_temporary_decoration(gcode_line) && GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) { - // remove temporary lines, add lines M73 where needed - unsigned int extra_lines_count = process_line_G1(g1_lines_counter++); - if (extra_lines_count > 0) - offsets.push_back({ line_id, extra_lines_count }); - } - - export_line += gcode_line; - if (export_line.length() > 65535) - write_string(export_line); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE gcode_line.clear(); } // Skip EOL. @@ -4140,30 +4014,12 @@ void GCodeProcessor::post_process() } } -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.flush(out, m_result, out_path); -#else - if (!export_line.empty()) - write_string(export_line); -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE out.close(); in.close(); -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE export_lines.synchronize_moves(m_result); -#else - // updates moves' gcode ids which have been modified by the insertion of the M73 lines - unsigned int curr_offset_id = 0; - unsigned int total_offset = 0; - for (GCodeProcessorResult::MoveVertex& move : m_result.moves) { - while (curr_offset_id < static_cast(offsets.size()) && offsets[curr_offset_id].first <= move.gcode_id) { - total_offset += offsets[curr_offset_id].second; - ++curr_offset_id; - } - move.gcode_id += total_offset; - } -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE if (rename_file(out_path, m_result.filename)) throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + m_result.filename + '\n' + diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 1f29488c3..26cb89894 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -127,9 +127,7 @@ namespace Slic3r { float max_print_height; SettingsIds settings_ids; size_t extruders_count; -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE bool backtrace_enabled; -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE std::vector extruder_colors; std::vector filament_diameters; std::vector filament_densities; diff --git a/src/libslic3r/GCodeReader.hpp b/src/libslic3r/GCodeReader.hpp index bf85a5131..58f55fdcf 100644 --- a/src/libslic3r/GCodeReader.hpp +++ b/src/libslic3r/GCodeReader.hpp @@ -71,7 +71,6 @@ public: return strncmp(cmd, cmd_test, len) == 0 && GCodeReader::is_end_of_word(cmd[len]); } -#if ENABLE_GCODE_POSTPROCESS_BACKTRACE static bool cmd_starts_with(const std::string& gcode_line, const char* cmd_test) { return strncmp(GCodeReader::skip_whitespaces(gcode_line.c_str()), cmd_test, strlen(cmd_test)) == 0; } @@ -82,7 +81,6 @@ public: const std::string_view cmd = temp.cmd(); return { cmd.begin(), cmd.end() }; } -#endif // ENABLE_GCODE_POSTPROCESS_BACKTRACE private: std::string m_raw; diff --git a/src/libslic3r/Technologies.hpp b/src/libslic3r/Technologies.hpp index c33f24311..60d89c9e9 100644 --- a/src/libslic3r/Technologies.hpp +++ b/src/libslic3r/Technologies.hpp @@ -58,8 +58,6 @@ // Enable alternative version of file_wildcards() #define ENABLE_ALTERNATIVE_FILE_WILDCARDS_GENERATOR (1 && ENABLE_2_6_0_ALPHA1) -// Enable gcode postprocess modified to allow for backward insertion of new lines -#define ENABLE_GCODE_POSTPROCESS_BACKTRACE (1 && ENABLE_2_6_0_ALPHA1) #endif // _prusaslicer_technologies_h_ From e3a868202d3e5aa6c843bb579886fab3c0e60c4a Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 26 Apr 2023 09:57:21 +0200 Subject: [PATCH 068/115] Fix for SPE-1681 - Sidebar: Text in support field can be deleted --- src/slic3r/GUI/Plater.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6adabb7d6..5657f30f2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -437,7 +437,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : support_def.label = L("Supports"); support_def.type = coStrings; support_def.tooltip = L("Select what kind of support do you need"); - support_def.set_enum_labels(ConfigOptionDef::GUIType::select_open, { + support_def.set_enum_labels(ConfigOptionDef::GUIType::select_close, { L("None"), L("Support on build plate only"), L("For support enforcers only"), @@ -592,7 +592,7 @@ FreqChangedParams::FreqChangedParams(wxWindow* parent) : pad_def.label = L("Pad"); pad_def.type = coStrings; pad_def.tooltip = L("Select what kind of pad do you need"); - pad_def.set_enum_labels(ConfigOptionDef::GUIType::select_open, { + pad_def.set_enum_labels(ConfigOptionDef::GUIType::select_close, { L("None"), L("Below object"), L("Around object") From cde530901ae5a6434206c8b3882d5618f8de9afe Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 26 Apr 2023 10:26:27 +0200 Subject: [PATCH 069/115] Follow up e3a868202d3e5aa6c843bb579886fab3c0e60c4a - fixed assert --- src/libslic3r/Config.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 538580d50..28410b87d 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -1991,7 +1991,7 @@ public: void set_enum_labels(GUIType gui_type, const std::initializer_list il) { this->enum_def_new(); - assert(gui_type == GUIType::i_enum_open || gui_type == GUIType::f_enum_open || gui_type == ConfigOptionDef::GUIType::select_open); + assert(gui_type == GUIType::i_enum_open || gui_type == GUIType::f_enum_open || gui_type == ConfigOptionDef::GUIType::select_close); this->gui_type = gui_type; enum_def->set_labels(il); } From cdf8cd83d5aa69c71e378c045a9037e77a16bb3d Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Wed, 26 Apr 2023 12:31:59 +0200 Subject: [PATCH 070/115] Follow-up of 6e7fefbabf067cfe1a54b638e16e494218432276 - Force using glGeneratedMipmap() function on AMD Custom cards, no matter what's the installed driver (Windows only) --- src/slic3r/GUI/OpenGLManager.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/OpenGLManager.cpp b/src/slic3r/GUI/OpenGLManager.cpp index 447da442e..f0713932d 100644 --- a/src/slic3r/GUI/OpenGLManager.cpp +++ b/src/slic3r/GUI/OpenGLManager.cpp @@ -419,9 +419,12 @@ bool OpenGLManager::init_gl() // There is no an easy way to detect the driver version without using Win32 API because the strings returned by OpenGL // have no standardized format, only some of them contain the driver version. // Until we do not know that driver will be fixed (if ever) we force the use of power of two textures on all cards - // containing the string 'Radeon' in the string returned by glGetString(GL_RENDERER) + // 1) containing the string 'Radeon' in the string returned by glGetString(GL_RENDERER) + // 2) containing the string 'Custom' in the string returned by glGetString(GL_RENDERER) const auto& gl_info = OpenGLManager::get_gl_info(); - if (boost::contains(gl_info.get_vendor(), "ATI Technologies Inc.") && boost::contains(gl_info.get_renderer(), "Radeon")) + if (boost::contains(gl_info.get_vendor(), "ATI Technologies Inc.") && + (boost::contains(gl_info.get_renderer(), "Radeon") || + boost::contains(gl_info.get_renderer(), "Custom"))) s_force_power_of_two_textures = true; #endif // _WIN32 } From 4c872b03528b292975bb60729076cbd6efb1054a Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 27 Apr 2023 15:36:21 +0200 Subject: [PATCH 071/115] Paralellize SupportSpotGenerator! Fix extra perimeters crash - problem with new ankerl hash map Fix progress bar --- src/libslic3r/PerimeterGenerator.cpp | 5 +- src/libslic3r/Print.cpp | 1 - src/libslic3r/SupportSpotsGenerator.cpp | 289 +++++++++++++----------- 3 files changed, 162 insertions(+), 133 deletions(-) diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 1a487cba8..e1068d763 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -681,7 +682,7 @@ Polylines reconnect_polylines(const Polylines &polylines, double limit_distance) if (polylines.empty()) return polylines; - ankerl::unordered_dense::map connected; + std::unordered_map connected; connected.reserve(polylines.size()); for (size_t i = 0; i < polylines.size(); i++) { if (!polylines[i].empty()) { @@ -731,7 +732,7 @@ ExtrusionPaths sort_extra_perimeters(ExtrusionPaths extra_perims, int index_of_f { if (extra_perims.empty()) return {}; - std::vector> dependencies(extra_perims.size()); + std::vector> dependencies(extra_perims.size()); for (size_t path_idx = 0; path_idx < extra_perims.size(); path_idx++) { for (size_t prev_path_idx = 0; prev_path_idx < path_idx; prev_path_idx++) { if (paths_touch(extra_perims[path_idx], extra_perims[prev_path_idx], extrusion_spacing * 1.5f)) { diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 49d17e5c0..cf8b5c577 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -880,7 +880,6 @@ void Print::process() BOOST_LOG_TRIVIAL(info) << "Starting the slicing process." << log_memory_info(); for (PrintObject *obj : m_objects) obj->make_perimeters(); - this->set_status(70, _u8L("Infilling layers")); for (PrintObject *obj : m_objects) obj->infill(); for (PrintObject *obj : m_objects) diff --git a/src/libslic3r/SupportSpotsGenerator.cpp b/src/libslic3r/SupportSpotsGenerator.cpp index e5748fad3..4c13cdd48 100644 --- a/src/libslic3r/SupportSpotsGenerator.cpp +++ b/src/libslic3r/SupportSpotsGenerator.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -172,6 +174,69 @@ struct SliceConnection } }; +SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) +{ + SliceConnection connection; + + const LayerSlice &slice = layer->lslices_ex[slice_idx]; + Polygons slice_polys = to_polygons(layer->lslices[slice_idx]); + BoundingBox slice_bb = get_extents(slice_polys); + const Layer *lower_layer = layer->lower_layer; + + ExPolygons below{}; + for (const auto &link : slice.overlaps_below) { below.push_back(lower_layer->lslices[link.slice_idx]); } + Polygons below_polys = to_polygons(below); + + BoundingBox below_bb = get_extents(below_polys); + + Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), + ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); + + for (const Polygon &poly : overlap) { + Vec2f p0 = unscaled(poly.first_point()).cast(); + for (size_t i = 2; i < poly.points.size(); i++) { + Vec2f p1 = unscaled(poly.points[i - 1]).cast(); + Vec2f p2 = unscaled(poly.points[i]).cast(); + + float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; + + auto [area, first_moment_of_area, second_moment_area, + second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); + connection.area += sign * area; + connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); + connection.second_moment_of_area_accumulator += sign * second_moment_area; + connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; + } + } + + return connection; +}; + +using PrecomputedSliceConnections = std::vector>; +PrecomputedSliceConnections precompute_slices_connections(const PrintObject *po) +{ + PrecomputedSliceConnections result{}; + for (size_t lidx = 0; lidx < po->layer_count(); lidx++) { + result.emplace_back(std::vector{}); + for (size_t slice_idx = 0; slice_idx < po->get_layer(lidx)->lslices_ex.size(); slice_idx++) { + result[lidx].push_back(SliceConnection{}); + } + } + + tbb::parallel_for(tbb::blocked_range(0, po->layers().size()), [po, &result](tbb::blocked_range r) { + for (size_t lidx = r.begin(); lidx < r.end(); lidx++) { + const Layer *l = po->get_layer(lidx); + tbb::parallel_for(tbb::blocked_range(0, l->lslices_ex.size()), [lidx, l, &result](tbb::blocked_range r2) { + for (size_t slice_idx = r2.begin(); slice_idx < r2.end(); slice_idx++) { + result[lidx][slice_idx] = estimate_slice_connection(slice_idx, l); + } + }); + } + }); + + return result; +}; + float get_flow_width(const LayerRegion *region, ExtrusionRole role) { if (role == ExtrusionRole::BridgeInfill) return region->flow(FlowRole::frExternalPerimeter).width(); @@ -253,15 +318,8 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit const AABBTreeLines::LinesDistancer &prev_layer_boundary, const Params ¶ms) { - if (entity->is_collection()) { - std::vector checked_lines_out; - checked_lines_out.reserve(prev_layer_lines.get_lines().size() / 3); - for (const auto *e : static_cast(entity)->entities) { - auto tmp = check_extrusion_entity_stability(e, layer_region, prev_layer_lines, prev_layer_boundary, params); - checked_lines_out.insert(checked_lines_out.end(), tmp.begin(), tmp.end()); - } - return checked_lines_out; - } else if (entity->role().is_bridge() && !entity->role().is_perimeter()) { + assert(!entity->is_collection()); + if (entity->role().is_bridge() && !entity->role().is_perimeter()) { // pure bridges are handled separately, beacuse we need to align the forward and backward direction support points if (entity->length() < scale_(params.min_distance_to_allow_local_supports)) { return {}; @@ -344,9 +402,10 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit curr_point.distance *= sign; SupportPointCause potential_cause = SupportPointCause::FloatingExtrusion; - if (bridged_distance + line_len > params.bridge_distance * 0.8 && std::abs(curr_point.curvature) < 0.1) { - potential_cause = SupportPointCause::FloatingExtrusion; - } + // Bridges are now separated. While long overhang perimeter is technically bridge, it would confuse the users + // if (bridged_distance + line_len > params.bridge_distance * 0.8 && std::abs(curr_point.curvature) < 0.1) { + // potential_cause = SupportPointCause::FloatingExtrusion; + // } float max_bridge_len = std::max(params.support_points_interface_radius * 2.0f, params.bridge_distance / @@ -383,44 +442,6 @@ std::vector check_extrusion_entity_stability(const ExtrusionEntit } } -SliceConnection estimate_slice_connection(size_t slice_idx, const Layer *layer) -{ - SliceConnection connection; - - const LayerSlice &slice = layer->lslices_ex[slice_idx]; - Polygons slice_polys = to_polygons(layer->lslices[slice_idx]); - BoundingBox slice_bb = get_extents(slice_polys); - const Layer *lower_layer = layer->lower_layer; - - ExPolygons below{}; - for (const auto &link : slice.overlaps_below) { below.push_back(lower_layer->lslices[link.slice_idx]); } - Polygons below_polys = to_polygons(below); - - BoundingBox below_bb = get_extents(below_polys); - - Polygons overlap = intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(slice_polys, below_bb), - ClipperUtils::clip_clipper_polygons_with_subject_bbox(below_polys, slice_bb)); - - for (const Polygon &poly : overlap) { - Vec2f p0 = unscaled(poly.first_point()).cast(); - for (size_t i = 2; i < poly.points.size(); i++) { - Vec2f p1 = unscaled(poly.points[i - 1]).cast(); - Vec2f p2 = unscaled(poly.points[i]).cast(); - - float sign = cross2(p1 - p0, p2 - p1) > 0 ? 1.0f : -1.0f; - - auto [area, first_moment_of_area, second_moment_area, - second_moment_of_area_covariance] = compute_moments_of_area_of_triangle(p0, p1, p2); - connection.area += sign * area; - connection.centroid_accumulator += sign * Vec3f(first_moment_of_area.x(), first_moment_of_area.y(), layer->print_z * area); - connection.second_moment_of_area_accumulator += sign * second_moment_area; - connection.second_moment_of_area_covariance_accumulator += sign * second_moment_of_area_covariance; - } - } - - return connection; -}; - class ObjectPart { public: @@ -761,7 +782,10 @@ public: } }; -std::tuple check_stability(const PrintObject *po, const PrintTryCancel &cancel_func, const Params ¶ms) +std::tuple check_stability(const PrintObject *po, + const PrecomputedSliceConnections &precomputed_slices_connections, + const PrintTryCancel &cancel_func, + const Params ¶ms) { SupportPoints supp_points{}; SupportGridFilter supports_presence_grid(po, params.min_distance_between_support_points); @@ -790,8 +814,8 @@ std::tuple check_stability(const PrintObject *po, for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { const LayerSlice &slice = layer->lslices_ex.at(slice_idx); - auto [new_part, covered_area] = build_object_part_from_slice(slice_idx, layer, params); - SliceConnection connection_to_below = estimate_slice_connection(slice_idx, layer); + auto [new_part, covered_area] = build_object_part_from_slice(slice_idx, layer, params); + const SliceConnection &connection_to_below = precomputed_slices_connections[layer_idx][slice_idx]; #ifdef DETAILED_DEBUG_LOGS std::cout << "SLICE IDX: " << slice_idx << std::endl; @@ -858,25 +882,87 @@ std::tuple check_stability(const PrintObject *po, prev_slice_idx_to_weakest_connection = next_slice_idx_to_weakest_connection; next_slice_idx_to_weakest_connection.clear(); + auto get_flat_entities = [](const ExtrusionEntity *e) { + std::vector entities; + std::vector queue{e}; + while (!queue.empty()) { + const ExtrusionEntity *next = queue.back(); + queue.pop_back(); + if (next->is_collection()) { + for (const ExtrusionEntity *e : static_cast(next)->entities) { + queue.push_back(e); + } + } else { + entities.push_back(next); + } + } + return entities; + }; + + struct EnitityToCheck + { + const ExtrusionEntity *e; + const LayerRegion *region; + size_t slice_idx; + }; + std::vector entities_to_check; + for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { + const LayerSlice &slice = layer->lslices_ex.at(slice_idx); + for (const auto &island : slice.islands) { + for (const LayerExtrusionRange &fill_range : island.fills) { + const LayerRegion *fill_region = layer->get_region(fill_range.region()); + for (const auto &fill_idx : fill_range) { + for (const ExtrusionEntity *e : get_flat_entities(fill_region->fills().entities[fill_idx])) { + if (e->role() == ExtrusionRole::BridgeInfill) { + entities_to_check.push_back({e, fill_region, slice_idx}); + } + } + } + } + + const LayerRegion *perimeter_region = layer->get_region(island.perimeters.region()); + for (const size_t &perimeter_idx : island.perimeters) { + for (const ExtrusionEntity *e : get_flat_entities(perimeter_region->perimeters().entities[perimeter_idx])) { + entities_to_check.push_back({e, perimeter_region, slice_idx}); + } + } + } + } + + AABBTreeLines::LinesDistancer prev_layer_boundary = layer->lower_layer != nullptr ? + AABBTreeLines::LinesDistancer{ + to_unscaled_linesf(layer->lower_layer->lslices)} : + AABBTreeLines::LinesDistancer{}; + + std::vector> unstable_lines_per_slice(layer->lslices_ex.size()); + std::vector> ext_perim_lines_per_slice(layer->lslices_ex.size()); + + tbb::parallel_for(tbb::blocked_range(0, entities_to_check.size()), + [&entities_to_check, &prev_layer_ext_perim_lines, &prev_layer_boundary, &unstable_lines_per_slice, + &ext_perim_lines_per_slice, ¶ms](tbb::blocked_range r) { + for (size_t entity_idx = r.begin(); entity_idx < r.end(); ++entity_idx) { + const auto &e_to_check = entities_to_check[entity_idx]; + for (const auto &line : + check_extrusion_entity_stability(e_to_check.e, e_to_check.region, prev_layer_ext_perim_lines, + prev_layer_boundary, params)) { + if (line.support_point_generated.has_value()) { + unstable_lines_per_slice[e_to_check.slice_idx].push_back(line); + } + if (line.is_external_perimeter()) { + ext_perim_lines_per_slice[e_to_check.slice_idx].push_back(line); + } + } + } + }); + std::vector current_layer_ext_perims_lines{}; current_layer_ext_perims_lines.reserve(prev_layer_ext_perim_lines.get_lines().size()); // All object parts updated, and for each slice we have coresponding weakest connection. // We can now check each slice and its corresponding weakest connection and object part for stability. for (size_t slice_idx = 0; slice_idx < layer->lslices_ex.size(); ++slice_idx) { - const LayerSlice &slice = layer->lslices_ex.at(slice_idx); ObjectPart &part = active_object_parts.access(prev_slice_idx_to_object_part_mapping[slice_idx]); SliceConnection &weakest_conn = prev_slice_idx_to_weakest_connection[slice_idx]; - std::vector boundary_lines; - for (const auto &link : slice.overlaps_below) { - auto ls = to_unscaled_linesf({layer->lower_layer->lslices[link.slice_idx]}); - boundary_lines.insert(boundary_lines.end(), ls.begin(), ls.end()); - } - AABBTreeLines::LinesDistancer prev_layer_boundary{std::move(boundary_lines)}; - - - std::vector current_slice_ext_perims_lines{}; - current_slice_ext_perims_lines.reserve(prev_layer_ext_perim_lines.get_lines().size() / layer->lslices_ex.size()); #ifdef DETAILED_DEBUG_LOGS weakest_conn.print_info("weakest connection info: "); #endif @@ -911,73 +997,15 @@ std::tuple check_stability(const PrintObject *po, } }; - // first we will check local extrusion stability of bridges, then of perimeters. Perimeters are more important, they - // account for most of the curling and possible crashes, so on them we will run also global stability check - for (const auto &island : slice.islands) { - // Support bridges where needed. - for (const LayerExtrusionRange &fill_range : island.fills) { - const LayerRegion *fill_region = layer->get_region(fill_range.region()); - for (const auto &fill_idx : fill_range) { - const ExtrusionEntity *entity = fill_region->fills().entities[fill_idx]; - if (entity->role() == ExtrusionRole::BridgeInfill) { - for (const ExtrusionLine &bridge : - check_extrusion_entity_stability(entity, fill_region, prev_layer_ext_perim_lines, prev_layer_boundary, - params)) { - if (bridge.support_point_generated.has_value()) { - reckon_new_support_point(*bridge.support_point_generated, create_support_point_position(bridge.b), - float(-EPSILON), Vec2f::Zero()); - } - } - } - } - } - - const LayerRegion *perimeter_region = layer->get_region(island.perimeters.region()); - for (const auto &perimeter_idx : island.perimeters) { - const ExtrusionEntity *entity = perimeter_region->perimeters().entities[perimeter_idx]; - std::vector perims = check_extrusion_entity_stability(entity, perimeter_region, - prev_layer_ext_perim_lines, prev_layer_boundary, - params); - for (const ExtrusionLine &perim : perims) { - if (perim.support_point_generated.has_value()) { - reckon_new_support_point(*perim.support_point_generated, create_support_point_position(perim.b), float(-EPSILON), - Vec2f::Zero()); - } - if (perim.is_external_perimeter()) { - current_slice_ext_perims_lines.push_back(perim); - } - } - } - // DEBUG EXPORT, NOT USED NOW - // if (BR_bridge) { - // Lines scaledl; - // for (const auto &l : prev_layer_boundary.get_lines()) { - // scaledl.emplace_back(Point::new_scale(l.a), Point::new_scale(l.b)); - // } - - // Lines perimsl; - // for (const auto &l : current_slice_ext_perims_lines) { - // perimsl.emplace_back(Point::new_scale(l.a), Point::new_scale(l.b)); - // } - - // BoundingBox bb = get_extents(scaledl); - // bb.merge(get_extents(perimsl)); - - // ::Slic3r::SVG svg(debug_out_path( - // ("slice" + std::to_string(slice_idx) + "_" + std::to_string(layer_idx).c_str()).c_str()), - // get_extents(scaledl)); - // svg.draw(scaledl, "red", scale_(0.4)); - // svg.draw(perimsl, "blue", scale_(0.25)); - - - // svg.Close(); - // } + for (const auto &l : unstable_lines_per_slice[slice_idx]) { + assert(l.support_point_generated.has_value()); + reckon_new_support_point(*l.support_point_generated, create_support_point_position(l.b), float(-EPSILON), Vec2f::Zero()); } - LD current_slice_lines_distancer(current_slice_ext_perims_lines); + LD current_slice_lines_distancer({ext_perim_lines_per_slice[slice_idx].begin(), ext_perim_lines_per_slice[slice_idx].end()}); float unchecked_dist = params.min_distance_between_support_points + 1.0f; - for (const ExtrusionLine &line : current_slice_ext_perims_lines) { + for (const ExtrusionLine &line : current_slice_lines_distancer.get_lines()) { if ((unchecked_dist + line.len < params.min_distance_between_support_points && line.curled_up_height < params.curling_tolerance_limit) || line.len < EPSILON) { unchecked_dist += line.len; @@ -993,8 +1021,8 @@ std::tuple check_stability(const PrintObject *po, } } } - current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), current_slice_ext_perims_lines.begin(), - current_slice_ext_perims_lines.end()); + current_layer_ext_perims_lines.insert(current_layer_ext_perims_lines.end(), current_slice_lines_distancer.get_lines().begin(), + current_slice_lines_distancer.get_lines().end()); } // slice iterations prev_layer_ext_perim_lines = LD(current_layer_ext_perims_lines); } // layer iterations @@ -1048,7 +1076,8 @@ void debug_export(const SupportPoints& support_points,const PartialObjects& obje std::tuple full_search(const PrintObject *po, const PrintTryCancel& cancel_func, const Params ¶ms) { - auto results = check_stability(po, cancel_func, params); + auto precomputed_slices_connections = precompute_slices_connections(po); + auto results = check_stability(po, precomputed_slices_connections, cancel_func, params); #ifdef DEBUG_FILES auto [supp_points, objects] = results; debug_export(supp_points, objects, "issues"); From 4b68fbd973069ca87d7aa5bd8881eb7f31c222d4 Mon Sep 17 00:00:00 2001 From: YuSanka Date: Thu, 27 Apr 2023 15:55:07 +0200 Subject: [PATCH 072/115] Follow-up https://github.com/Prusa-Development/PrusaSlicerPrivate/commit/88f4fa20df3e73b962b7800cc709b0ed43113338 - Fixed next Linux specific issue : Select any instance in place where is active a gizmo space on instance --- src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp | 18 ++---------------- src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp | 1 - 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp index 98058ee17..d10c7a5ff 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.cpp @@ -24,14 +24,8 @@ GLGizmoFlatten::GLGizmoFlatten(GLCanvas3D& parent, const std::string& icon_filen bool GLGizmoFlatten::on_mouse(const wxMouseEvent &mouse_event) { - if (mouse_event.Moving()) { - // only for sure - m_mouse_left_down = false; - return false; - } if (mouse_event.LeftDown()) { if (m_hover_id != -1) { - m_mouse_left_down = true; Selection &selection = m_parent.get_selection(); if (selection.is_single_full_instance()) { // Rotate the object so the normal points downward: @@ -42,16 +36,8 @@ bool GLGizmoFlatten::on_mouse(const wxMouseEvent &mouse_event) return true; } } - else if (mouse_event.LeftUp()) { - if (m_mouse_left_down) { - // responsible for mouse left up after selecting plane - m_mouse_left_down = false; - return true; - } - - } - else if (mouse_event.Leaving()) - m_mouse_left_down = false; + else if (mouse_event.LeftUp()) + return m_hover_id != -1; return false; } diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp index ff44fcd2d..e9e3c08d0 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFlatten.hpp @@ -37,7 +37,6 @@ private: std::vector m_planes; std::vector> m_planes_casters; - bool m_mouse_left_down = false; // for detection left_up of this gizmo const ModelObject* m_old_model_object = nullptr; int m_old_instance_id{ -1 }; From 03608580c04842898031b5b3eeb1548443998592 Mon Sep 17 00:00:00 2001 From: enricoturri1966 Date: Tue, 25 Apr 2023 12:14:06 +0200 Subject: [PATCH 073/115] Do not allow to change selection or printable state, using the right panel, while the SLA supports gizmo is open and in editing mode. --- src/slic3r/GUI/GUI_ObjectList.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index a2ecbaea9..85d12bec9 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -85,6 +85,17 @@ ObjectList::ObjectList(wxWindow* parent) : // describe control behavior Bind(wxEVT_DATAVIEW_SELECTION_CHANGED, [this](wxDataViewEvent& event) { + // do not allow to change selection while the sla support gizmo is in editing mode + const GLGizmosManager& gizmos = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); + if (gizmos.get_current_type() == GLGizmosManager::EType::SlaSupports && gizmos.is_in_editing_mode(true)) { + wxDataViewItemArray sels; + GetSelections(sels); + if (sels.size() > 1 || event.GetItem() != m_last_selected_item) { + select_item(m_last_selected_item); + return; + } + } + // detect the current mouse position here, to pass it to list_manipulation() method // if we detect it later, the user may have moved the mouse pointer while calculations are performed, and this would mess-up the HitTest() call performed into list_manipulation() // see: https://github.com/prusa3d/PrusaSlicer/issues/3802 @@ -4960,6 +4971,11 @@ void ObjectList::update_printable_state(int obj_idx, int instance_idx) void ObjectList::toggle_printable_state() { + // do not allow to toggle the printable state while the sla support gizmo is in editing mode + const GLGizmosManager& gizmos = wxGetApp().plater()->canvas3D()->get_gizmos_manager(); + if (gizmos.get_current_type() == GLGizmosManager::EType::SlaSupports && gizmos.is_in_editing_mode(true)) + return; + wxDataViewItemArray sels; GetSelections(sels); if (sels.IsEmpty()) From a7e17df25f66d3fb4fce213574ba25ebea1d238d Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 12:59:20 +0200 Subject: [PATCH 074/115] ClipperLib: Further optimization of memory allocation using scalable_allocator. ClipperLib: SimplifyPolygon() - changed default winding number to positive, added strictly_simple parameter. ClipperUtlis simplify_polygons() - removed "remove_collinear" parameter --- src/clipper/clipper.cpp | 206 +++++++++++------------- src/clipper/clipper.hpp | 43 +++-- src/libslic3r/Arachne/WallToolPaths.cpp | 4 +- src/libslic3r/ClipperUtils.cpp | 25 ++- src/libslic3r/ClipperUtils.hpp | 4 +- 5 files changed, 137 insertions(+), 145 deletions(-) diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index c775a3226..5da79d3a1 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -73,25 +73,6 @@ static int const Skip = -2; //edge that would otherwise close a path #define TOLERANCE (1.0e-20) #define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) -// Output polygon. -struct OutRec { - int Idx; - bool IsHole; - bool IsOpen; - //The 'FirstLeft' field points to another OutRec that contains or is the - //'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is - //parsed left from the current edge (owning OutRec) until the owner OutRec - //is found. This field simplifies sorting the polygons into a tree structure - //which reflects the parent/child relationships of all polygons. - //This field should be renamed Parent, and will be later. - OutRec *FirstLeft; - // Used only by void Clipper::BuildResult2(PolyTree& polytree) - PolyNode *PolyNd; - // Linked list of output points, dynamically allocated. - OutPt *Pts; - OutPt *BottomPt; -}; - //------------------------------------------------------------------------------ inline IntPoint IntPoint2d(cInt x, cInt y) @@ -1061,8 +1042,7 @@ IntRect ClipperBase::GetBounds() Clipper::Clipper(int initOptions) : ClipperBase(), m_OutPtsFree(nullptr), - m_OutPtsChunkSize(32), - m_OutPtsChunkLast(32), + m_OutPtsChunkLast(m_OutPtsChunkSize), m_ActiveEdges(nullptr), m_SortedEdges(nullptr) { @@ -1153,23 +1133,23 @@ bool Clipper::ExecuteInternal() //FIXME Vojtech: Does it not invalidate the loop hierarchy maintained as OutRec::FirstLeft pointers? //FIXME Vojtech: The area is calculated with floats, it may not be numerically stable! { - for (OutRec *outRec : m_PolyOuts) - if (outRec->Pts && !outRec->IsOpen && (outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) - ReversePolyPtLinks(outRec->Pts); + for (OutRec &outRec : m_PolyOuts) + if (outRec.Pts && !outRec.IsOpen && (outRec.IsHole ^ m_ReverseOutput) == (Area(outRec) > 0)) + ReversePolyPtLinks(outRec.Pts); } JoinCommonEdges(); //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() { - for (OutRec *outRec : m_PolyOuts) - if (outRec->Pts) { - if (outRec->IsOpen) + for (OutRec &outRec : m_PolyOuts) + if (outRec.Pts) { + if (outRec.IsOpen) // Removes duplicate points. - FixupOutPolyline(*outRec); + FixupOutPolyline(outRec); else // Removes duplicate points and simplifies consecutive parallel edges by removing the middle vertex. - FixupOutPolygon(*outRec); + FixupOutPolygon(outRec); } } // For each polygon, search for exactly duplicate non-successive points. @@ -1194,22 +1174,18 @@ OutPt* Clipper::AllocateOutPt() m_OutPtsFree = pt->Next; } else if (m_OutPtsChunkLast < m_OutPtsChunkSize) { // Get a point from the last chunk. - pt = m_OutPts.back() + (m_OutPtsChunkLast ++); + pt = &m_OutPts.back()[m_OutPtsChunkLast ++]; } else { // The last chunk is full. Allocate a new one. - m_OutPts.push_back(new OutPt[m_OutPtsChunkSize]); + m_OutPts.push_back({}); m_OutPtsChunkLast = 1; - pt = m_OutPts.back(); + pt = &m_OutPts.back().front(); } return pt; } void Clipper::DisposeAllOutRecs() { - for (OutPt *pts : m_OutPts) - delete[] pts; - for (OutRec *rec : m_PolyOuts) - delete rec; m_OutPts.clear(); m_OutPtsFree = nullptr; m_OutPtsChunkLast = m_OutPtsChunkSize; @@ -1832,7 +1808,7 @@ void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) } //------------------------------------------------------------------------------ -void Clipper::SetHoleState(TEdge *e, OutRec *outrec) const +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { bool IsHole = false; TEdge *e2 = e->PrevInAEL; @@ -1842,7 +1818,7 @@ void Clipper::SetHoleState(TEdge *e, OutRec *outrec) const { IsHole = !IsHole; if (! outrec->FirstLeft) - outrec->FirstLeft = m_PolyOuts[e2->OutIdx]; + outrec->FirstLeft = &m_PolyOuts[e2->OutIdx]; } e2 = e2->PrevInAEL; } @@ -1883,18 +1859,18 @@ bool Param1RightOfParam2(OutRec* outRec1, OutRec* outRec2) OutRec* Clipper::GetOutRec(int Idx) { - OutRec* outrec = m_PolyOuts[Idx]; - while (outrec != m_PolyOuts[outrec->Idx]) - outrec = m_PolyOuts[outrec->Idx]; + OutRec* outrec = &m_PolyOuts[Idx]; + while (outrec != &m_PolyOuts[outrec->Idx]) + outrec = &m_PolyOuts[outrec->Idx]; return outrec; } //------------------------------------------------------------------------------ -void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) const +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { //get the start and ends of both output polygons ... - OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; - OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + OutRec *outRec1 = &m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = &m_PolyOuts[e2->OutIdx]; OutRec *holeStateRec; if (Param1RightOfParam2(outRec1, outRec2)) @@ -1991,16 +1967,16 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) const OutRec* Clipper::CreateOutRec() { - OutRec* result = new OutRec; - result->IsHole = false; - result->IsOpen = false; - result->FirstLeft = 0; - result->Pts = 0; - result->BottomPt = 0; - result->PolyNd = 0; - m_PolyOuts.push_back(result); - result->Idx = (int)m_PolyOuts.size()-1; - return result; + m_PolyOuts.push_back({}); + OutRec &result = m_PolyOuts.back(); + result.IsHole = false; + result.IsOpen = false; + result.FirstLeft = 0; + result.Pts = 0; + result.BottomPt = 0; + result.PolyNd = 0; + result.Idx = (int)m_PolyOuts.size()-1; + return &result; } //------------------------------------------------------------------------------ @@ -2022,7 +1998,7 @@ OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) return newOp; } else { - OutRec *outRec = m_PolyOuts[e->OutIdx]; + OutRec *outRec = &m_PolyOuts[e->OutIdx]; //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' OutPt* op = outRec->Pts; @@ -2045,7 +2021,7 @@ OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) OutPt* Clipper::GetLastOutPt(TEdge *e) { - OutRec *outRec = m_PolyOuts[e->OutIdx]; + OutRec *outRec = &m_PolyOuts[e->OutIdx]; if (e->Side == esLeft) return outRec->Pts; else @@ -2216,7 +2192,7 @@ void Clipper::ProcessHorizontal(TEdge *horzEdge) { Direction dir; cInt horzLeft, horzRight; - bool IsOpen = (horzEdge->OutIdx >= 0 && m_PolyOuts[horzEdge->OutIdx]->IsOpen); + bool IsOpen = (horzEdge->OutIdx >= 0 && m_PolyOuts[horzEdge->OutIdx].IsOpen); GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); @@ -2778,12 +2754,12 @@ int PointCount(OutPt *Pts) void Clipper::BuildResult(Paths &polys) { polys.reserve(m_PolyOuts.size()); - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - assert(! outRec->IsOpen); - if (!outRec->Pts) continue; + assert(! outRec.IsOpen); + if (!outRec.Pts) continue; Path pg; - OutPt* p = outRec->Pts->Prev; + OutPt* p = outRec.Pts->Prev; int cnt = PointCount(p); if (cnt < 2) continue; pg.reserve(cnt); @@ -2802,31 +2778,31 @@ void Clipper::BuildResult2(PolyTree& polytree) polytree.Clear(); polytree.AllNodes.reserve(m_PolyOuts.size()); //add each output polygon/contour to polytree ... - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - int cnt = PointCount(outRec->Pts); - if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) + int cnt = PointCount(outRec.Pts); + if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) // Ignore an invalid output loop or a polyline. continue; //skip OutRecs that (a) contain outermost polygons or //(b) already have the correct owner/child linkage ... - if (outRec->FirstLeft && - (outRec->IsHole == outRec->FirstLeft->IsHole || ! outRec->FirstLeft->Pts)) { - OutRec* orfl = outRec->FirstLeft; - while (orfl && ((orfl->IsHole == outRec->IsHole) || !orfl->Pts)) + if (outRec.FirstLeft && + (outRec.IsHole == outRec.FirstLeft->IsHole || ! outRec.FirstLeft->Pts)) { + OutRec* orfl = outRec.FirstLeft; + while (orfl && ((orfl->IsHole == outRec.IsHole) || !orfl->Pts)) orfl = orfl->FirstLeft; - outRec->FirstLeft = orfl; + outRec.FirstLeft = orfl; } //nb: polytree takes ownership of all the PolyNodes polytree.AllNodes.emplace_back(PolyNode()); PolyNode* pn = &polytree.AllNodes.back(); - outRec->PolyNd = pn; + outRec.PolyNd = pn; pn->Parent = 0; pn->Index = 0; pn->Contour.reserve(cnt); - OutPt *op = outRec->Pts->Prev; + OutPt *op = outRec.Pts->Prev; for (int j = 0; j < cnt; j++) { pn->Contour.emplace_back(op->Pt); @@ -2836,18 +2812,18 @@ void Clipper::BuildResult2(PolyTree& polytree) //fixup PolyNode links etc ... polytree.Childs.reserve(m_PolyOuts.size()); - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - if (!outRec->PolyNd) continue; - if (outRec->IsOpen) + if (!outRec.PolyNd) continue; + if (outRec.IsOpen) { - outRec->PolyNd->m_IsOpen = true; - polytree.AddChild(*outRec->PolyNd); + outRec.PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec.PolyNd); } - else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) - outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else if (outRec.FirstLeft && outRec.FirstLeft->PolyNd) + outRec.FirstLeft->PolyNd->AddChild(*outRec.PolyNd); else - polytree.AddChild(*outRec->PolyNd); + polytree.AddChild(*outRec.PolyNd); } } //------------------------------------------------------------------------------ @@ -3193,26 +3169,26 @@ bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) //---------------------------------------------------------------------- // This is potentially very expensive! O(n^3)! -void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) const +void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) { //tests if NewOutRec contains the polygon before reassigning FirstLeft - for (OutRec *outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - if (!outRec->Pts || !outRec->FirstLeft) continue; - OutRec* firstLeft = outRec->FirstLeft; + if (!outRec.Pts || !outRec.FirstLeft) continue; + OutRec* firstLeft = outRec.FirstLeft; // Skip empty polygons. while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (firstLeft == OldOutRec && Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) - outRec->FirstLeft = NewOutRec; + if (firstLeft == OldOutRec && Poly2ContainsPoly1(outRec.Pts, NewOutRec->Pts)) + outRec.FirstLeft = NewOutRec; } } //---------------------------------------------------------------------- -void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) const +void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) { //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon - for (OutRec *outRec : m_PolyOuts) - if (outRec->FirstLeft == OldOutRec) outRec->FirstLeft = NewOutRec; + for (OutRec &outRec : m_PolyOuts) + if (outRec.FirstLeft == OldOutRec) outRec.FirstLeft = NewOutRec; } //---------------------------------------------------------------------- @@ -3253,13 +3229,13 @@ void Clipper::JoinCommonEdges() if (m_UsingPolyTree) for (size_t j = 0; j < m_PolyOuts.size() - 1; j++) { - OutRec* oRec = m_PolyOuts[j]; - OutRec* firstLeft = oRec->FirstLeft; + OutRec &oRec = m_PolyOuts[j]; + OutRec* firstLeft = oRec.FirstLeft; while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (!oRec->Pts || firstLeft != outRec1 || - oRec->IsHole == outRec1->IsHole) continue; - if (Poly2ContainsPoly1(oRec->Pts, join.OutPt2)) - oRec->FirstLeft = outRec2; + if (!oRec.Pts || firstLeft != outRec1 || + oRec.IsHole == outRec1->IsHole) continue; + if (Poly2ContainsPoly1(oRec.Pts, join.OutPt2)) + oRec.FirstLeft = outRec2; } if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) @@ -3771,13 +3747,13 @@ void Clipper::DoSimplePolygons() size_t i = 0; while (i < m_PolyOuts.size()) { - OutRec* outrec = m_PolyOuts[i++]; - OutPt* op = outrec->Pts; - if (!op || outrec->IsOpen) continue; + OutRec &outrec = m_PolyOuts[i++]; + OutPt* op = outrec.Pts; + if (!op || outrec.IsOpen) continue; do //for each Pt in Polygon until duplicate found do ... { OutPt* op2 = op->Next; - while (op2 != outrec->Pts) + while (op2 != outrec.Pts) { if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { @@ -3789,37 +3765,37 @@ void Clipper::DoSimplePolygons() op2->Prev = op3; op3->Next = op2; - outrec->Pts = op; + outrec.Pts = op; OutRec* outrec2 = CreateOutRec(); outrec2->Pts = op2; UpdateOutPtIdxs(*outrec2); - if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) + if (Poly2ContainsPoly1(outrec2->Pts, outrec.Pts)) { //OutRec2 is contained by OutRec1 ... - outrec2->IsHole = !outrec->IsHole; - outrec2->FirstLeft = outrec; + outrec2->IsHole = !outrec.IsHole; + outrec2->FirstLeft = &outrec; // For each m_PolyOuts, replace FirstLeft from outRec2 to outrec. - if (m_UsingPolyTree) FixupFirstLefts2(outrec2, outrec); + if (m_UsingPolyTree) FixupFirstLefts2(outrec2, &outrec); } else - if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) + if (Poly2ContainsPoly1(outrec.Pts, outrec2->Pts)) { //OutRec1 is contained by OutRec2 ... - outrec2->IsHole = outrec->IsHole; - outrec->IsHole = !outrec2->IsHole; - outrec2->FirstLeft = outrec->FirstLeft; - outrec->FirstLeft = outrec2; + outrec2->IsHole = outrec.IsHole; + outrec.IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec.FirstLeft; + outrec.FirstLeft = outrec2; // For each m_PolyOuts, replace FirstLeft from outrec to outrec2. - if (m_UsingPolyTree) FixupFirstLefts2(outrec, outrec2); + if (m_UsingPolyTree) FixupFirstLefts2(&outrec, outrec2); } else { //the 2 polygons are separate ... - outrec2->IsHole = outrec->IsHole; - outrec2->FirstLeft = outrec->FirstLeft; + outrec2->IsHole = outrec.IsHole; + outrec2->FirstLeft = outrec.FirstLeft; // For each polygon of m_PolyOuts, replace FirstLeft from outrec to outrec2 if the polygon is inside outRec2. //FIXME This is potentially very expensive! O(n^3)! - if (m_UsingPolyTree) FixupFirstLefts1(outrec, outrec2); + if (m_UsingPolyTree) FixupFirstLefts1(&outrec, outrec2); } op2 = op; //ie get ready for the Next iteration } @@ -3827,7 +3803,7 @@ void Clipper::DoSimplePolygons() } op = op->Next; } - while (op != outrec->Pts); + while (op != outrec.Pts); } } //------------------------------------------------------------------------------ @@ -3845,10 +3821,10 @@ void ReversePaths(Paths& p) } //------------------------------------------------------------------------------ -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType) +Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType, bool strictly_simple /* = true */) { Clipper c; - c.StrictlySimple(true); + c.StrictlySimple(strictly_simple); c.AddPath(in_poly, ptSubject, true); Paths out; c.Execute(ctUnion, out, fillType, fillType); diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index d190d09b5..c88545454 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -52,6 +52,7 @@ //use_deprecated: Enables temporary support for the obsolete functions //#define use_deprecated +#include #include #include #include @@ -199,7 +200,8 @@ double Area(const Path &poly); inline bool Orientation(const Path &poly) { return Area(poly) >= 0; } int PointInPolygon(const IntPoint &pt, const Path &path); -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftEvenOdd); +// Union with "strictly simple" fix enabled. +Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftNonZero, bool strictly_simple = true); void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415); void CleanPolygon(Path& poly, double distance = 1.415); @@ -284,7 +286,25 @@ enum EdgeSide { esLeft = 1, esRight = 2}; using OutPts = std::vector>; - struct OutRec; + // Output polygon. + struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + //The 'FirstLeft' field points to another OutRec that contains or is the + //'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is + //parsed left from the current edge (owning OutRec) until the owner OutRec + //is found. This field simplifies sorting the polygons into a tree structure + //which reflects the parent/child relationships of all polygons. + //This field should be renamed Parent, and will be later. + OutRec* FirstLeft; + // Used only by void Clipper::BuildResult2(PolyTree& polytree) + PolyNode* PolyNd; + // Linked list of output points, dynamically allocated. + OutPt* Pts; + OutPt* BottomPt; + }; + struct Join { Join(OutPt *OutPt1, OutPt *OutPt2, IntPoint OffPt) : OutPt1(OutPt1), OutPt2(OutPt2), OffPt(OffPt) {} @@ -432,12 +452,12 @@ protected: private: // Output polygons. - std::vector> m_PolyOuts; + std::deque> m_PolyOuts; // Output points, allocated by a continuous sets of m_OutPtsChunkSize. - std::vector> m_OutPts; + static constexpr const size_t m_OutPtsChunkSize = 32; + std::deque, Allocator>> m_OutPts; // List of free output points, to be used before taking a point from m_OutPts or allocating a new chunk. OutPt *m_OutPtsFree; - size_t m_OutPtsChunkSize; size_t m_OutPtsChunkLast; std::vector> m_Joins; @@ -482,7 +502,7 @@ private: void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutRec* GetOutRec(int idx); - void AppendPolygon(TEdge *e1, TEdge *e2) const; + void AppendPolygon(TEdge *e1, TEdge *e2); void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); OutRec* CreateOutRec(); OutPt* AddOutPt(TEdge *e, const IntPoint &pt); @@ -498,7 +518,7 @@ private: void ProcessEdgesAtTopOfScanbeam(const cInt topY); void BuildResult(Paths& polys); void BuildResult2(PolyTree& polytree); - void SetHoleState(TEdge *e, OutRec *outrec) const; + void SetHoleState(TEdge *e, OutRec *outrec); bool FixupIntersectionOrder(); void FixupOutPolygon(OutRec &outrec); void FixupOutPolyline(OutRec &outrec); @@ -508,8 +528,8 @@ private: bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, const IntPoint &Pt, bool DiscardLeft); void JoinCommonEdges(); void DoSimplePolygons(); - void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) const; - void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) const; + void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); + void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec); #ifdef CLIPPERLIB_USE_XYZ void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); #endif @@ -567,10 +587,11 @@ class clipperException : public std::exception }; //------------------------------------------------------------------------------ +// Union with "strictly simple" fix enabled. template -inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftEvenOdd) { +inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftNonZero, bool strictly_simple = true) { Clipper c; - c.StrictlySimple(true); + c.StrictlySimple(strictly_simple); c.AddPaths(std::forward(in_polys), ptSubject, true); Paths out; c.Execute(ctUnion, out, fillType, fillType); diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index fce69d5e4..6c5dafdac 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -232,7 +232,7 @@ std::unique_ptr cre void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) { if (epsilon < 1) { - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); return; } @@ -273,7 +273,7 @@ void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) } } - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); } /*! diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 95a533718..d10e14cc5 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -964,21 +964,18 @@ Polygons union_pt_chained_outside_in(const Polygons &subject) return retval; } -Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) +Polygons simplify_polygons(const Polygons &subject) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); ClipperLib::Paths output; - if (preserve_collinear) { - ClipperLib::Clipper c; - c.PreserveCollinear(true); - c.StrictlySimple(true); - c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); - c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - } else { - output = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(subject), ClipperLib::pftNonZero); - } - + ClipperLib::Clipper c; +// c.PreserveCollinear(true); + //FIXME StrictlySimple is very expensive! Is it needed? + c.StrictlySimple(true); + c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); + c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + // convert into Slic3r polygons return to_polygons(std::move(output)); } @@ -987,12 +984,10 @@ ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); - if (! preserve_collinear) - return union_ex(simplify_polygons(subject, false)); - ClipperLib::PolyTree polytree; ClipperLib::Clipper c; - c.PreserveCollinear(true); +// c.PreserveCollinear(true); + //FIXME StrictlySimple is very expensive! Is it needed? c.StrictlySimple(true); c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); c.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index aaa06107d..ab967fd8f 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -596,8 +596,8 @@ void traverse_pt(const ClipperLib::PolyNodes &nodes, ExOrJustPolygons *retval) /* OTHER */ -Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject, bool preserve_collinear = false); -Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject, bool preserve_collinear = false); +Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject); +Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject); Polygons top_level_islands(const Slic3r::Polygons &polygons); From 0836b06c7360462e3b8719864de4a446887c0d9e Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 15:48:24 +0200 Subject: [PATCH 075/115] Little refactoring of douglas_peucker() --- src/libslic3r/ExPolygon.cpp | 4 ++-- src/libslic3r/Geometry.cpp | 16 ++++++------- src/libslic3r/MultiPoint.cpp | 2 +- src/libslic3r/MultiPoint.hpp | 2 +- src/libslic3r/Polygon.cpp | 45 +++++++++++++++++++++++++----------- src/libslic3r/Polygon.hpp | 3 ++- src/libslic3r/Polyline.cpp | 2 +- 7 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 42f026e0b..19489bddb 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -184,14 +184,14 @@ Polygons ExPolygon::simplify_p(double tolerance) const { Polygon p = this->contour; p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points = MultiPoint::douglas_peucker(p.points, tolerance); p.points.pop_back(); pp.emplace_back(std::move(p)); } // holes for (Polygon p : this->holes) { p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points = MultiPoint::douglas_peucker(p.points, tolerance); p.points.pop_back(); pp.emplace_back(std::move(p)); } diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 5542d73ee..f2860ea8e 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -52,15 +52,15 @@ template bool contains(const ExPolygons &vector, const Point &point); void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval) { - Polygons pp; - for (Polygons::const_iterator it = polygons.begin(); it != polygons.end(); ++it) { - Polygon p = *it; - p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); - p.points.pop_back(); - pp.push_back(p); + Polygons simplified_raw; + for (const Polygon &source_polygon : polygons) { + Points simplified = MultiPoint::douglas_peucker(to_polyline(source_polygon).points, tolerance); + if (simplified.size() > 3) { + simplified.pop_back(); + simplified_raw.push_back(Polygon{ std::move(simplified) }); + } } - *retval = Slic3r::simplify_polygons(pp); + *retval = Slic3r::simplify_polygons(simplified_raw); } double linint(double value, double oldmin, double oldmax, double newmin, double newmax) diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index bb4d62cc0..ce54a54c0 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -103,7 +103,7 @@ bool MultiPoint::remove_duplicate_points() return false; } -Points MultiPoint::_douglas_peucker(const Points &pts, const double tolerance) +Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) { Points result_pts; double tolerance_sq = tolerance * tolerance; diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index 4cf4b5e14..62b53255b 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -81,7 +81,7 @@ public: } } - static Points _douglas_peucker(const Points &points, const double tolerance); + static Points douglas_peucker(const Points &points, const double tolerance); static Points visivalingam(const Points& pts, const double& tolerance); inline auto begin() { return points.begin(); } diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 299e22adc..88ac1b03f 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -96,7 +96,7 @@ bool Polygon::make_clockwise() void Polygon::douglas_peucker(double tolerance) { this->points.push_back(this->points.front()); - Points p = MultiPoint::_douglas_peucker(this->points, tolerance); + Points p = MultiPoint::douglas_peucker(this->points, tolerance); p.pop_back(); this->points = std::move(p); } @@ -110,7 +110,7 @@ Polygons Polygon::simplify(double tolerance) const // on the whole polygon Points points = this->points; points.push_back(points.front()); - Polygon p(MultiPoint::_douglas_peucker(points, tolerance)); + Polygon p(MultiPoint::douglas_peucker(points, tolerance)); p.points.pop_back(); Polygons pp; @@ -577,23 +577,40 @@ void remove_collinear(Polygons &polys) remove_collinear(poly); } -Polygons polygons_simplify(const Polygons &source_polygons, double tolerance) +static inline void simplify_polygon_impl(const Points &points, double tolerance, bool strictly_simple, Polygons &out) +{ + Points simplified = MultiPoint::douglas_peucker(points, tolerance); + // then remove the last (repeated) point. + simplified.pop_back(); + // Simplify the decimated contour by ClipperLib. + bool ccw = ClipperLib::Area(simplified) > 0.; + for (Points& path : ClipperLib::SimplifyPolygons(ClipperUtils::SinglePathProvider(simplified), ClipperLib::pftNonZero, strictly_simple)) { + if (!ccw) + // ClipperLib likely reoriented negative area contours to become positive. Reverse holes back to CW. + std::reverse(path.begin(), path.end()); + out.emplace_back(std::move(path)); + } +} + +Polygons polygons_simplify(Polygons &&source_polygons, double tolerance, bool strictly_simple /* = true */) +{ + Polygons out; + out.reserve(source_polygons.size()); + for (Polygon &source_polygon : source_polygons) { + // Run Douglas / Peucker simplification algorithm on an open polyline (by repeating the first point at the end of the polyline), + source_polygon.points.emplace_back(source_polygon.points.front()); + simplify_polygon_impl(source_polygon.points, tolerance, strictly_simple, out); + } + return out; +} + +Polygons polygons_simplify(const Polygons &source_polygons, double tolerance, bool strictly_simple /* = true */) { Polygons out; out.reserve(source_polygons.size()); for (const Polygon &source_polygon : source_polygons) { // Run Douglas / Peucker simplification algorithm on an open polyline (by repeating the first point at the end of the polyline), - Points simplified = MultiPoint::_douglas_peucker(to_polyline(source_polygon).points, tolerance); - // then remove the last (repeated) point. - simplified.pop_back(); - // Simplify the decimated contour by ClipperLib. - bool ccw = ClipperLib::Area(simplified) > 0.; - for (Points &path : ClipperLib::SimplifyPolygons(ClipperUtils::SinglePathProvider(simplified), ClipperLib::pftNonZero)) { - if (! ccw) - // ClipperLib likely reoriented negative area contours to become positive. Reverse holes back to CW. - std::reverse(path.begin(), path.end()); - out.emplace_back(std::move(path)); - } + simplify_polygon_impl(to_polyline(source_polygon).points, tolerance, strictly_simple, out); } return out; } diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index bf4a087b0..e0c3958fd 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -149,7 +149,8 @@ inline void polygons_append(Polygons &dst, Polygons &&src) } } -Polygons polygons_simplify(const Polygons &polys, double tolerance); +Polygons polygons_simplify(Polygons &&polys, double tolerance, bool strictly_simple = true); +Polygons polygons_simplify(const Polygons &polys, double tolerance, bool strictly_simple = true); inline void polygons_rotate(Polygons &polys, double angle) { diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 5743e38bd..524736575 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -110,7 +110,7 @@ Points Polyline::equally_spaced_points(double distance) const void Polyline::simplify(double tolerance) { - this->points = MultiPoint::_douglas_peucker(this->points, tolerance); + this->points = MultiPoint::douglas_peucker(this->points, tolerance); } #if 0 From 31a5daa5e4ab315d668b1b63f1691ae4961235a6 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 15:53:08 +0200 Subject: [PATCH 076/115] Optimization of triangle mesh slicing: scalable_allocator and hashing of shared mutexes. --- src/libslic3r/TriangleMeshSlicer.cpp | 42 ++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 460cd901e..0261b6121 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -15,6 +15,9 @@ #include #include +#include + +#include #ifndef NDEBUG // #define EXPENSIVE_DEBUG_CHECKS @@ -139,7 +142,7 @@ public: #endif }; -using IntersectionLines = std::vector; +using IntersectionLines = std::vector>; enum class FacetSliceType { NoSlice = 0, @@ -351,6 +354,21 @@ inline FacetSliceType slice_facet( return FacetSliceType::NoSlice; } +class LinesMutexes { +public: + std::mutex& operator()(size_t slice_id) { + ankerl::unordered_dense::hash hash; + return m_mutexes[hash(slice_id) % m_mutexes.size()].mutex; + } + +private: + struct CacheLineAlignedMutex + { + alignas(std::hardware_destructive_interference_size) std::mutex mutex; + }; + std::array m_mutexes; +}; + template void slice_facet_at_zs( // Scaled or unscaled vertices. transform_vertex_fn may scale zs. @@ -361,7 +379,7 @@ void slice_facet_at_zs( // Scaled or unscaled zs. If vertices have their zs scaled or transform_vertex_fn scales them, then zs have to be scaled as well. const std::vector &zs, std::vector &lines, - std::array &lines_mutex) + LinesMutexes &lines_mutex) { stl_vertex vertices[3] { transform_vertex_fn(mesh_vertices[indices(0)]), transform_vertex_fn(mesh_vertices[indices(1)]), transform_vertex_fn(mesh_vertices[indices(2)]) }; @@ -380,7 +398,7 @@ void slice_facet_at_zs( if (min_z != max_z && slice_facet(*it, vertices, indices, edge_ids, idx_vertex_lowest, false, il) == FacetSliceType::Slicing) { assert(il.edge_type != IntersectionLine::FacetEdgeType::Horizontal); size_t slice_id = it - zs.begin(); - boost::lock_guard l(lines_mutex[slice_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(slice_id)); lines[slice_id].emplace_back(il); } } @@ -395,8 +413,8 @@ static inline std::vector slice_make_lines( const std::vector &zs, const ThrowOnCancel throw_on_cancel_fn) { - std::vector lines(zs.size(), IntersectionLines()); - std::array lines_mutex; + std::vector lines(zs.size(), IntersectionLines{}); + LinesMutexes lines_mutex; tbb::parallel_for( tbb::blocked_range(0, int(indices.size())), [&vertices, &transform_vertex_fn, &indices, &face_edge_ids, &zs, &lines, &lines_mutex, throw_on_cancel_fn](const tbb::blocked_range &range) { @@ -475,7 +493,7 @@ void slice_facet_with_slabs( const int num_edges, const std::vector &zs, SlabLines &lines, - std::array &lines_mutex) + LinesMutexes &lines_mutex) { const stl_triangle_vertex_indices &indices = mesh_triangles[facet_idx]; stl_vertex vertices[3] { mesh_vertices[indices(0)], mesh_vertices[indices(1)], mesh_vertices[indices(2)] }; @@ -494,7 +512,7 @@ void slice_facet_with_slabs( auto emit_slab_edge = [&lines, &lines_mutex](IntersectionLine il, size_t slab_id, bool reverse) { if (reverse) il.reverse(); - boost::lock_guard l(lines_mutex[(slab_id + lines_mutex.size() / 2) % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(slab_id)); lines.between_slices[slab_id].emplace_back(il); }; @@ -530,7 +548,7 @@ void slice_facet_with_slabs( }; // Don't flip the FacetEdgeType::Top edge, it will be flipped when chaining. // if (! ProjectionFromTop) il.reverse(); - boost::lock_guard l(lines_mutex[line_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(line_id)); lines.at_slice[line_id].emplace_back(il); } } else { @@ -649,7 +667,7 @@ void slice_facet_with_slabs( if (! ProjectionFromTop) il.reverse(); size_t line_id = it - zs.begin(); - boost::lock_guard l(lines_mutex[line_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(line_id)); lines.at_slice[line_id].emplace_back(il); } } @@ -804,8 +822,8 @@ inline std::pair slice_slabs_make_lines( std::pair out; SlabLines &lines_top = out.first; SlabLines &lines_bottom = out.second; - std::array lines_mutex_top; - std::array lines_mutex_bottom; + LinesMutexes lines_mutex_top; + LinesMutexes lines_mutex_bottom; if (top) { lines_top.at_slice.assign(zs.size(), IntersectionLines()); @@ -1540,7 +1558,7 @@ static std::vector make_slab_loops( } // Used to cut the mesh into two halves. -static ExPolygons make_expolygons_simple(std::vector &lines) +static ExPolygons make_expolygons_simple(IntersectionLines &lines) { ExPolygons slices; Polygons holes; From 04f557693b8369d7019e861a4bf26bab44ee4464 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 16:26:16 +0200 Subject: [PATCH 077/115] Follow-up to 31a5daa5e4ab315d668b1b63f1691ae4961235a6: Fixed missing include --- src/libslic3r/TriangleMeshSlicer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 0261b6121..2c6570219 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include From a077f669ee5409c7550df538dd00c6b294d7019f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 16:32:03 +0200 Subject: [PATCH 078/115] Slight optimization of distance_to_squared() --- src/libslic3r/Line.hpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/Line.hpp b/src/libslic3r/Line.hpp index 1edd1f5e9..d90757bed 100644 --- a/src/libslic3r/Line.hpp +++ b/src/libslic3r/Line.hpp @@ -40,11 +40,12 @@ template auto get_b(L &&l) { return Traits>::get_b(l) // Distance to the closest point of line. template -double distance_to_squared(const L &line, const Vec, Scalar> &point, Vec, Scalar> *nearest_point) +inline double distance_to_squared(const L &line, const Vec, Scalar> &point, Vec, Scalar> *nearest_point) { - const Vec, double> v = (get_b(line) - get_a(line)).template cast(); - const Vec, double> va = (point - get_a(line)).template cast(); - const double l2 = v.squaredNorm(); // avoid a sqrt + using VecType = Vec, double>; + const VecType v = (get_b(line) - get_a(line)).template cast(); + const VecType va = (point - get_a(line)).template cast(); + const double l2 = v.squaredNorm(); if (l2 == 0.0) { // a == b case *nearest_point = get_a(line); @@ -53,19 +54,20 @@ double distance_to_squared(const L &line, const Vec, Scalar> &point, V // Consider the line extending the segment, parameterized as a + t (b - a). // We find projection of this point onto the line. // It falls where t = [(this-a) . (b-a)] / |b-a|^2 - const double t = va.dot(v) / l2; + const double t = va.dot(v); if (t <= 0.0) { // beyond the 'a' end of the segment *nearest_point = get_a(line); return va.squaredNorm(); - } else if (t >= 1.0) { + } else if (t >= l2) { // beyond the 'b' end of the segment *nearest_point = get_b(line); return (point - get_b(line)).template cast().squaredNorm(); } - *nearest_point = (get_a(line).template cast() + t * v).template cast>(); - return (t * v - va).squaredNorm(); + const VecType w = ((t / l2) * v).eval(); + *nearest_point = (get_a(line).template cast() + w).template cast>(); + return (w - va).squaredNorm(); } // Distance to the closest point of line. From e7f4704ddc1c64a55b1a9bc57ecb3ffc3e6631b6 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 16:35:39 +0200 Subject: [PATCH 079/115] Organic supports, TreeModelVolumes: Cheaper simplification. --- src/libslic3r/TreeModelVolumes.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index 5a1fcec56..3c82b5c6c 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -34,6 +34,8 @@ using namespace std::literals; // had to use a define beacuse the macro processing inside macro BOOST_LOG_TRIVIAL() #define error_level_not_in_cache error +static constexpr const bool polygons_strictly_simple = false; + TreeSupportMeshGroupSettings::TreeSupportMeshGroupSettings(const PrintObject &print_object) { const PrintConfig &print_config = print_object.print()->config(); @@ -175,7 +177,7 @@ TreeModelVolumes::TreeModelVolumes( tbb::parallel_for(tbb::blocked_range(num_raft_layers, num_layers, std::min(1, std::max(16, num_layers / (8 * tbb::this_task_arena::max_concurrency())))), [&](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) - outlines[layer_idx] = to_polygons(expolygons_simplify(print_object.get_layer(layer_idx - num_raft_layers)->lslices, mesh_settings.resolution)); + outlines[layer_idx] = polygons_simplify(to_polygons(print_object.get_layer(layer_idx - num_raft_layers)->lslices), mesh_settings.resolution, polygons_strictly_simple); }); } #endif @@ -585,7 +587,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex if (processing_last_mesh) { if (! dst.empty()) collisions = union_(collisions, dst); - dst = polygons_simplify(collisions, min_resolution); + dst = polygons_simplify(collisions, min_resolution, polygons_strictly_simple); } else append(dst, std::move(collisions)); throw_on_cancel(); @@ -609,7 +611,7 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex if (processing_last_mesh) { if (! dst.empty()) placable = union_(placable, dst); - dst = polygons_simplify(placable, min_resolution); + dst = polygons_simplify(placable, min_resolution, polygons_strictly_simple); } else append(dst, placable); throw_on_cancel(); @@ -657,7 +659,7 @@ void TreeModelVolumes::calculateCollisionHolefree(const std::vectorgetCollision(m_increase_until_radius, layer_idx, false)), 5 - increase_radius_ceil, ClipperLib::jtRound, m_min_resolution), - m_min_resolution)); + m_min_resolution, polygons_strictly_simple)); throw_on_cancel(); } } @@ -744,7 +746,7 @@ void TreeModelVolumes::calculateAvoidance(const std::vector &ke ClipperLib::jtRound, m_min_resolution)); if (task.to_model) latest_avoidance = diff(latest_avoidance, getPlaceableAreas(task.radius, layer_idx, throw_on_cancel)); - latest_avoidance = polygons_simplify(latest_avoidance, m_min_resolution); + latest_avoidance = polygons_simplify(latest_avoidance, m_min_resolution, polygons_strictly_simple); data.emplace_back(RadiusLayerPair{task.radius, layer_idx}, latest_avoidance); throw_on_cancel(); } @@ -865,12 +867,12 @@ void TreeModelVolumes::calculateWallRestrictions(const std::vector Date: Tue, 2 May 2023 18:17:08 +0200 Subject: [PATCH 080/115] ClipperLib: emplace_back() instead of push_back(). --- src/clipper/clipper.cpp | 90 ++++++++++++++++++++--------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index 5da79d3a1..3691877ee 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -112,7 +112,7 @@ int PolyTree::Total() const void PolyNode::AddChild(PolyNode& child) { unsigned cnt = (unsigned)Childs.size(); - Childs.push_back(&child); + Childs.emplace_back(&child); child.Parent = this; child.Index = cnt; } @@ -674,7 +674,7 @@ TEdge* ClipperBase::ProcessBound(TEdge* E, bool NextIsForward) locMin.RightBound = E; E->WindDelta = 0; Result = ProcessBound(E, NextIsForward); - m_MinimaList.push_back(locMin); + m_MinimaList.emplace_back(locMin); } return Result; } @@ -896,7 +896,7 @@ bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, b E->NextInLML = E->Next; E = E->Next; } - m_MinimaList.push_back(locMin); + m_MinimaList.emplace_back(locMin); return true; } @@ -949,7 +949,7 @@ bool ClipperBase::AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, b locMin.LeftBound = 0; else if (locMin.RightBound->OutIdx == Skip) locMin.RightBound = 0; - m_MinimaList.push_back(locMin); + m_MinimaList.emplace_back(locMin); if (!leftBoundIsForward) E = E2; } return true; @@ -1177,7 +1177,7 @@ OutPt* Clipper::AllocateOutPt() pt = &m_OutPts.back()[m_OutPtsChunkLast ++]; } else { // The last chunk is full. Allocate a new one. - m_OutPts.push_back({}); + m_OutPts.emplace_back(); m_OutPtsChunkLast = 1; pt = &m_OutPts.back().front(); } @@ -1967,7 +1967,7 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) OutRec* Clipper::CreateOutRec() { - m_PolyOuts.push_back({}); + m_PolyOuts.emplace_back(); OutRec &result = m_PolyOuts.back(); result.IsHole = false; result.IsOpen = false; @@ -2576,7 +2576,7 @@ void Clipper::ProcessEdgesAtTopOfScanbeam(const cInt topY) if(IsMaximaEdge) { - if (m_StrictSimple) m_Maxima.push_back(e->Top.x()); + if (m_StrictSimple) m_Maxima.emplace_back(e->Top.x()); TEdge* ePrev = e->PrevInAEL; DoMaxima(e); if( !ePrev ) e = m_ActiveEdges; @@ -3349,7 +3349,7 @@ void ClipperOffset::AddPath(const Path& path, JoinType joinType, EndType endType break; } newNode->Contour.reserve(highI + 1); - newNode->Contour.push_back(path[0]); + newNode->Contour.emplace_back(path[0]); int j = 0, k = 0; for (int i = 1; i <= highI; i++) { bool same = false; @@ -3362,7 +3362,7 @@ void ClipperOffset::AddPath(const Path& path, JoinType joinType, EndType endType if (same) continue; j++; - newNode->Contour.push_back(path[i]); + newNode->Contour.emplace_back(path[i]); if (path[i].y() > newNode->Contour[k].y() || (path[i].y() == newNode->Contour[k].y() && path[i].x() < newNode->Contour[k].x())) k = j; @@ -3490,7 +3490,7 @@ void ClipperOffset::DoOffset(double delta) { PolyNode& node = *m_polyNodes.Childs[i]; if (node.m_endtype == etClosedPolygon) - m_destPolys.push_back(node.Contour); + m_destPolys.emplace_back(node.Contour); } return; } @@ -3532,7 +3532,7 @@ void ClipperOffset::DoOffset(double delta) double X = 1.0, Y = 0.0; for (cInt j = 1; j <= steps; j++) { - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[0].x() + X * delta), Round(m_srcPoly[0].y() + Y * delta))); double X2 = X; @@ -3545,7 +3545,7 @@ void ClipperOffset::DoOffset(double delta) double X = -1.0, Y = -1.0; for (int j = 0; j < 4; ++j) { - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[0].x() + X * delta), Round(m_srcPoly[0].y() + Y * delta))); if (X < 0) X = 1; @@ -3553,32 +3553,32 @@ void ClipperOffset::DoOffset(double delta) else X = -1; } } - m_destPolys.push_back(m_destPoly); + m_destPolys.emplace_back(m_destPoly); continue; } //build m_normals ... m_normals.clear(); m_normals.reserve(len); for (int j = 0; j < len - 1; ++j) - m_normals.push_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); + m_normals.emplace_back(GetUnitNormal(m_srcPoly[j], m_srcPoly[j + 1])); if (node.m_endtype == etClosedLine || node.m_endtype == etClosedPolygon) - m_normals.push_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); + m_normals.emplace_back(GetUnitNormal(m_srcPoly[len - 1], m_srcPoly[0])); else - m_normals.push_back(DoublePoint(m_normals[len - 2])); + m_normals.emplace_back(DoublePoint(m_normals[len - 2])); if (node.m_endtype == etClosedPolygon) { int k = len - 1; for (int j = 0; j < len; ++j) OffsetPoint(j, k, node.m_jointype); - m_destPolys.push_back(m_destPoly); + m_destPolys.emplace_back(m_destPoly); } else if (node.m_endtype == etClosedLine) { int k = len - 1; for (int j = 0; j < len; ++j) OffsetPoint(j, k, node.m_jointype); - m_destPolys.push_back(m_destPoly); + m_destPolys.emplace_back(m_destPoly); m_destPoly.clear(); //re-build m_normals ... DoublePoint n = m_normals[len -1]; @@ -3588,7 +3588,7 @@ void ClipperOffset::DoOffset(double delta) k = 0; for (int j = len - 1; j >= 0; j--) OffsetPoint(j, k, node.m_jointype); - m_destPolys.push_back(m_destPoly); + m_destPolys.emplace_back(m_destPoly); } else { @@ -3601,9 +3601,9 @@ void ClipperOffset::DoOffset(double delta) { int j = len - 1; pt1 = IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * delta), Round(m_srcPoly[j].y() + m_normals[j].y() * delta)); - m_destPoly.push_back(pt1); + m_destPoly.emplace_back(pt1); pt1 = IntPoint2d(Round(m_srcPoly[j].x() - m_normals[j].x() * delta), Round(m_srcPoly[j].y() - m_normals[j].y() * delta)); - m_destPoly.push_back(pt1); + m_destPoly.emplace_back(pt1); } else { @@ -3628,9 +3628,9 @@ void ClipperOffset::DoOffset(double delta) if (node.m_endtype == etOpenButt) { pt1 = IntPoint2d(Round(m_srcPoly[0].x() - m_normals[0].x() * delta), Round(m_srcPoly[0].y() - m_normals[0].y() * delta)); - m_destPoly.push_back(pt1); + m_destPoly.emplace_back(pt1); pt1 = IntPoint2d(Round(m_srcPoly[0].x() + m_normals[0].x() * delta), Round(m_srcPoly[0].y() + m_normals[0].y() * delta)); - m_destPoly.push_back(pt1); + m_destPoly.emplace_back(pt1); } else { @@ -3641,7 +3641,7 @@ void ClipperOffset::DoOffset(double delta) else DoRound(0, 1); } - m_destPolys.push_back(m_destPoly); + m_destPolys.emplace_back(m_destPoly); } } } @@ -3657,7 +3657,7 @@ void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) double cosA = (m_normals[k].x() * m_normals[j].x() + m_normals[j].y() * m_normals[k].y() ); if (cosA > 0) // angle => 0 degrees { - m_destPoly.push_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); return; } @@ -3668,10 +3668,10 @@ void ClipperOffset::OffsetPoint(int j, int& k, JoinType jointype) if (m_sinA * m_delta < 0) { - m_destPoly.push_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[k].x() * m_delta), Round(m_srcPoly[j].y() + m_normals[k].y() * m_delta))); - m_destPoly.push_back(m_srcPoly[j]); - m_destPoly.push_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), + m_destPoly.emplace_back(m_srcPoly[j]); + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); } else @@ -3695,10 +3695,10 @@ void ClipperOffset::DoSquare(int j, int k) { double dx = std::tan(std::atan2(m_sinA, m_normals[k].x() * m_normals[j].x() + m_normals[k].y() * m_normals[j].y()) / 4); - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[j].x() + m_delta * (m_normals[k].x() - m_normals[k].y() * dx)), Round(m_srcPoly[j].y() + m_delta * (m_normals[k].y() + m_normals[k].x() * dx)))); - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[j].x() + m_delta * (m_normals[j].x() + m_normals[j].y() * dx)), Round(m_srcPoly[j].y() + m_delta * (m_normals[j].y() - m_normals[j].x() * dx)))); } @@ -3707,7 +3707,7 @@ void ClipperOffset::DoSquare(int j, int k) void ClipperOffset::DoMiter(int j, int k, double r) { double q = m_delta / r; - m_destPoly.push_back(IntPoint2d(Round(m_srcPoly[j].x() + (m_normals[k].x() + m_normals[j].x()) * q), + m_destPoly.emplace_back(IntPoint2d(Round(m_srcPoly[j].x() + (m_normals[k].x() + m_normals[j].x()) * q), Round(m_srcPoly[j].y() + (m_normals[k].y() + m_normals[j].y()) * q))); } //------------------------------------------------------------------------------ @@ -3721,14 +3721,14 @@ void ClipperOffset::DoRound(int j, int k) double X = m_normals[k].x(), Y = m_normals[k].y(), X2; for (int i = 0; i < steps; ++i) { - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[j].x() + X * m_delta), Round(m_srcPoly[j].y() + Y * m_delta))); X2 = X; X = X * m_cos - m_sin * Y; Y = X2 * m_sin + Y * m_cos; } - m_destPoly.push_back(IntPoint2d( + m_destPoly.emplace_back(IntPoint2d( Round(m_srcPoly[j].x() + m_normals[j].x() * m_delta), Round(m_srcPoly[j].y() + m_normals[j].y() * m_delta))); } @@ -3996,8 +3996,8 @@ void Minkowski(const Path& poly, const Path& path, Path p; p.reserve(polyCnt); for (size_t j = 0; j < poly.size(); ++j) - p.push_back(IntPoint2d(path[i].x() + poly[j].x(), path[i].y() + poly[j].y())); - pp.push_back(p); + p.emplace_back(IntPoint2d(path[i].x() + poly[j].x(), path[i].y() + poly[j].y())); + pp.emplace_back(p); } else for (size_t i = 0; i < pathCnt; ++i) @@ -4005,8 +4005,8 @@ void Minkowski(const Path& poly, const Path& path, Path p; p.reserve(polyCnt); for (size_t j = 0; j < poly.size(); ++j) - p.push_back(IntPoint2d(path[i].x() - poly[j].x(), path[i].y() - poly[j].y())); - pp.push_back(p); + p.emplace_back(IntPoint2d(path[i].x() - poly[j].x(), path[i].y() - poly[j].y())); + pp.emplace_back(p); } solution.clear(); @@ -4016,12 +4016,12 @@ void Minkowski(const Path& poly, const Path& path, { Path quad; quad.reserve(4); - quad.push_back(pp[i % pathCnt][j % polyCnt]); - quad.push_back(pp[(i + 1) % pathCnt][j % polyCnt]); - quad.push_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); - quad.push_back(pp[i % pathCnt][(j + 1) % polyCnt]); + quad.emplace_back(pp[i % pathCnt][j % polyCnt]); + quad.emplace_back(pp[(i + 1) % pathCnt][j % polyCnt]); + quad.emplace_back(pp[(i + 1) % pathCnt][(j + 1) % polyCnt]); + quad.emplace_back(pp[i % pathCnt][(j + 1) % polyCnt]); if (!Orientation(quad)) ReversePath(quad); - solution.push_back(quad); + solution.emplace_back(quad); } } //------------------------------------------------------------------------------ @@ -4081,7 +4081,7 @@ void AddPolyNodeToPaths(const PolyNode& polynode, NodeType nodetype, Paths& path else if (nodetype == ntOpen) return; if (!polynode.Contour.empty() && match) - paths.push_back(polynode.Contour); + paths.emplace_back(polynode.Contour); for (int i = 0; i < polynode.ChildCount(); ++i) AddPolyNodeToPaths(*polynode.Childs[i], nodetype, paths); } @@ -4093,7 +4093,7 @@ void AddPolyNodeToPaths(PolyNode&& polynode, NodeType nodetype, Paths& paths) else if (nodetype == ntOpen) return; if (!polynode.Contour.empty() && match) - paths.push_back(std::move(polynode.Contour)); + paths.emplace_back(std::move(polynode.Contour)); for (int i = 0; i < polynode.ChildCount(); ++i) AddPolyNodeToPaths(std::move(*polynode.Childs[i]), nodetype, paths); } @@ -4131,7 +4131,7 @@ void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths) //Open paths are top level only, so ... for (int i = 0; i < polytree.ChildCount(); ++i) if (polytree.Childs[i]->IsOpen()) - paths.push_back(polytree.Childs[i]->Contour); + paths.emplace_back(polytree.Childs[i]->Contour); } //------------------------------------------------------------------------------ From 63ca221394acca695508dd333633b43b1a5e9744 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 18:20:38 +0200 Subject: [PATCH 081/115] douglas_peucker(): Optimized for 32bit Point types. --- src/libslic3r/MultiPoint.cpp | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index ce54a54c0..90928d3c1 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -106,7 +106,7 @@ bool MultiPoint::remove_duplicate_points() Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) { Points result_pts; - double tolerance_sq = tolerance * tolerance; + auto tolerance_sq = int64_t(sqr(tolerance)); if (! pts.empty()) { const Point *anchor = &pts.front(); size_t anchor_idx = 0; @@ -120,14 +120,42 @@ Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) dpStack.reserve(pts.size()); dpStack.emplace_back(floater_idx); for (;;) { - double max_dist_sq = 0.0; - size_t furthest_idx = anchor_idx; + int64_t max_dist_sq = 0; + size_t furthest_idx = anchor_idx; // find point furthest from line seg created by (anchor, floater) and note it - for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) { - double dist_sq = Line::distance_to_squared(pts[i], *anchor, *floater); - if (dist_sq > max_dist_sq) { - max_dist_sq = dist_sq; - furthest_idx = i; + { + const Point a = *anchor; + const Point f = *floater; + const Vec2i64 v = (f - a).cast(); + const int64_t l2 = v.squaredNorm(); + // Make up for rounding when converting from int64_t to double. Double mantissa is just 52 bits. + if (l2 < (1 << 14)) { + for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) + if (int64_t dist_sq = (pts[i] - a).cast().squaredNorm(); dist_sq > max_dist_sq) { + max_dist_sq = dist_sq; + furthest_idx = i; + } + } else { + const double dl2 = double(l2); + const Vec2d dv = v.cast(); + for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) { + const Point p = pts[i]; + const Vec2i64 va = (p - a).template cast(); + const int64_t t = va.dot(v); + int64_t dist_sq; + if (t <= 0) { + dist_sq = va.squaredNorm(); + } else if (t >= l2) { + dist_sq = (p - f).cast().squaredNorm(); + } else { + const Vec2i64 w = ((double(t) / dl2) * dv).cast(); + dist_sq = (w - va).squaredNorm(); + } + if (dist_sq > max_dist_sq) { + max_dist_sq = dist_sq; + furthest_idx = i; + } + } } } // remove point if less than tolerance From 2600ba71ad52d3f43d3efd244493458567a7b457 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 18:36:59 +0200 Subject: [PATCH 082/115] Fix of undefined hardware_destructive_interference_size --- src/libslic3r/TriangleMeshSlicer.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 2c6570219..747f36215 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -36,6 +36,13 @@ #include #include +#ifdef __cpp_lib_hardware_interference_size + using std::hardware_destructive_interference_size; +#else + // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ... + constexpr std::size_t hardware_destructive_interference_size = 64; +#endif + // #define SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING @@ -365,7 +372,7 @@ public: private: struct CacheLineAlignedMutex { - alignas(std::hardware_destructive_interference_size) std::mutex mutex; + alignas(hardware_destructive_interference_size) std::mutex mutex; }; std::array m_mutexes; }; From 15ccecf8858f2f5e11da62b5ccb1b25300b27538 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 18:37:24 +0200 Subject: [PATCH 083/115] TreeSupport optimization: better parallel scaling, simpler simplification --- src/libslic3r/TreeSupport.cpp | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 174e91a9c..3452bb71c 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -59,6 +59,8 @@ namespace Slic3r namespace FFFTreeSupport { +static constexpr const bool polygons_strictly_simple = false; + TreeSupportSettings::TreeSupportSettings(const TreeSupportMeshGroupSettings& mesh_group_settings, const SlicingParameters &slicing_params) : angle(mesh_group_settings.support_tree_angle), angle_slow(mesh_group_settings.support_tree_angle_slow), @@ -925,13 +927,13 @@ static std::optional> polyline_sample_next_point_at_dis ret = diff(offset(ret, step_size, ClipperLib::jtRound, scaled(0.01)), collision_trimmed()); // ensure that if many offsets are done the performance does not suffer extremely by the new vertices of jtRound. if (i % 10 == 7) - ret = polygons_simplify(ret, scaled(0.015)); + ret = polygons_simplify(ret, scaled(0.015), polygons_strictly_simple); } // offset the remainder float last_offset = distance - steps * step_size; if (last_offset > SCALED_EPSILON) ret = offset(ret, distance - steps * step_size, ClipperLib::jtRound, scaled(0.01)); - ret = polygons_simplify(ret, scaled(0.015)); + ret = polygons_simplify(ret, scaled(0.015), polygons_strictly_simple); if (do_final_difference) ret = diff(ret, collision_trimmed()); @@ -1797,7 +1799,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di } if (settings.no_error && settings.move) // as ClipperLib::jtRound has to be used for offsets this simplify is VERY important for performance. - polygons_simplify(increased, scaled(0.025)); + polygons_simplify(increased, scaled(0.025), polygons_strictly_simple); } else // if no movement is done the areas keep parent area as no move == offset(0) increased = parent.influence_area; @@ -1821,6 +1823,7 @@ static Point move_inside_if_outside(const Polygons &polygons, Point from, int di BOOST_LOG_TRIVIAL(debug) << "Corrected taint leading to a wrong non gracious value on layer " << layer_idx - 1 << " targeting " << current_elem.target_height << " with radius " << radius; } else + // Cannot route to gracious areas. Push the tree away from object and route it down anyways. to_model_data = safe_union(diff_clipped(increased, volumes.getCollision(radius, layer_idx - 1, settings.use_min_distance))); } } @@ -1966,7 +1969,7 @@ static void increase_areas_one_layer( { using AvoidanceType = TreeModelVolumes::AvoidanceType; - tbb::parallel_for(tbb::blocked_range(0, merging_areas.size()), + tbb::parallel_for(tbb::blocked_range(0, merging_areas.size(), 1), [&](const tbb::blocked_range &range) { for (size_t merging_area_idx = range.begin(); merging_area_idx < range.end(); ++ merging_area_idx) { SupportElementMerging &merging_area = merging_areas[merging_area_idx]; @@ -2209,9 +2212,10 @@ static void increase_areas_one_layer( // A point can be set on the top most tip layer (maybe more if it should not move for a few layers). parent.state.result_on_layer_reset(); } + throw_on_cancel(); } - }); + }, tbb::simple_partitioner()); } [[nodiscard]] static SupportElementState merge_support_element_states( @@ -3325,7 +3329,7 @@ static void finalize_interface_and_support_areas( base_layer_polygons = smooth_outward(union_(base_layer_polygons), config.support_line_width); //FIXME was .smooth(50); //smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : // simplify a bit, to ensure the output does not contain outrageous amounts of vertices. Should not be necessary, just a precaution. - base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled(0.03), double(config.resolution))); + base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled(0.03), double(config.resolution)), polygons_strictly_simple); } if (! support_roof_polygons.empty() && ! base_layer_polygons.empty()) { @@ -4270,7 +4274,7 @@ static std::vector draw_branches( const SlicingParameters &slicing_params = print_object.slicing_parameters(); MeshSlicingParams mesh_slicing_params; mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; - tbb::parallel_for(tbb::blocked_range(0, trees.size()), + tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), [&trees, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; std::vector slice_z; @@ -4322,9 +4326,9 @@ static std::vector draw_branches( tree.first_layer_id = new_begin; } } - }); + }, tbb::simple_partitioner()); - tbb::parallel_for(tbb::blocked_range(0, trees.size()), + tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), [&trees, &throw_on_cancel](const tbb::blocked_range &range) { for (size_t tree_id = range.begin(); tree_id < range.end(); ++ tree_id) { Tree &tree = trees[tree_id]; @@ -4335,7 +4339,7 @@ static std::vector draw_branches( } throw_on_cancel(); } - }); + }, tbb::simple_partitioner()); size_t num_layers = 0; for (Tree &tree : trees) @@ -4356,14 +4360,14 @@ static std::vector draw_branches( } std::vector support_layer_storage(move_bounds.size()); - tbb::parallel_for(tbb::blocked_range(0, std::min(move_bounds.size(), slices.size())), + tbb::parallel_for(tbb::blocked_range(0, std::min(move_bounds.size(), slices.size()), 1), [&slices, &support_layer_storage, &throw_on_cancel](const tbb::blocked_range &range) { for (size_t slice_id = range.begin(); slice_id < range.end(); ++ slice_id) { Slice &slice = slices[slice_id]; support_layer_storage[slice_id] = slice.num_branches > 1 ? union_(slice.polygons) : std::move(slice.polygons); throw_on_cancel(); } - }); + }, tbb::simple_partitioner()); //FIXME simplify! return support_layer_storage; From 0c6f2261a3518ef2ae5337e34179632c503d4f48 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 18:45:07 +0200 Subject: [PATCH 084/115] Tree Supports: Extend bottoms of trees downwards to the object surface if those supports are not sitting on a flat surface. --- src/libslic3r/TreeSupport.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 3452bb71c..820b60d49 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -4275,7 +4275,7 @@ static std::vector draw_branches( MeshSlicingParams mesh_slicing_params; mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), - [&trees, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { + [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; std::vector slice_z; for (size_t tree_id = range.begin(); tree_id < range.end(); ++ tree_id) { @@ -4297,7 +4297,23 @@ static std::vector draw_branches( slice_z.emplace_back(float(0.5 * (bottom_z + print_z))); } std::vector slices = slice_mesh(partial_mesh, slice_z, mesh_slicing_params, throw_on_cancel); - size_t num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); + size_t num_empty = 0; + + if (layer_begin > 0 && branch.has_root && ! branch.path.front()->state.to_model_gracious && ! slices.front().empty()) { + // Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something. + std::vector bottom_extra_slices; + Polygons rest_support; + for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= 0; -- layer_idx) { + rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + if (area(rest_support) < tiny_area_threshold) + break; + bottom_extra_slices.emplace_back(rest_support); + } + layer_begin -= LayerIndex(bottom_extra_slices.size()); + slices.insert(slices.begin(), std::make_move_iterator(bottom_extra_slices.rbegin()), std::make_move_iterator(bottom_extra_slices.rend())); + } else + num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); + layer_begin += LayerIndex(num_empty); for (; slices.back().empty(); -- layer_end); LayerIndex new_begin = tree.first_layer_id == -1 ? layer_begin : std::min(tree.first_layer_id, layer_begin); From d0f38cd0b4c65e0b2b2e91d9df3669fc3f65bdd8 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 19:26:33 +0200 Subject: [PATCH 085/115] macOS clang is buggy, it does not implement __cpp_lib_hardware_interference_size correctly. --- src/libslic3r/TriangleMeshSlicer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 747f36215..da696e1ec 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -36,7 +36,7 @@ #include #include -#ifdef __cpp_lib_hardware_interference_size +#if defined(__cpp_lib_hardware_interference_size) && ! defined(__APPLE__) using std::hardware_destructive_interference_size; #else // 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │ ... From af6c27b8613959319bbb66d1f066e55c2e231b1e Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 19:50:12 +0200 Subject: [PATCH 086/115] douglas_peucker(): fix after 63ca221394acca695508dd333633b43b1a5e9744 --- src/libslic3r/MultiPoint.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index 90928d3c1..fb4727abe 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -124,12 +124,10 @@ Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) size_t furthest_idx = anchor_idx; // find point furthest from line seg created by (anchor, floater) and note it { - const Point a = *anchor; - const Point f = *floater; + const Point a = *anchor; + const Point f = *floater; const Vec2i64 v = (f - a).cast(); - const int64_t l2 = v.squaredNorm(); - // Make up for rounding when converting from int64_t to double. Double mantissa is just 52 bits. - if (l2 < (1 << 14)) { + if (const int64_t l2 = v.squaredNorm(); l2 == 0) { for (size_t i = anchor_idx + 1; i < floater_idx; ++ i) if (int64_t dist_sq = (pts[i] - a).cast().squaredNorm(); dist_sq > max_dist_sq) { max_dist_sq = dist_sq; From 6831b7094a47a1dfcc6a283b9156f3026adf0b0c Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Mon, 3 Apr 2023 16:02:26 +0200 Subject: [PATCH 087/115] Trying to fix transformation assembly problem for arrange polyogon --- src/libslic3r/Model.cpp | 13 +++++++------ src/libslic3r/Model.hpp | 13 ++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 6c350e7b4..f0ad7e1e0 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2406,11 +2406,12 @@ arrangement::ArrangePolygon ModelInstance::get_arrange_polygon() const { // static const double SIMPLIFY_TOLERANCE_MM = 0.1; - Vec3d rotation = get_rotation(); - rotation.z() = 0.; - Transform3d trafo_instance = Geometry::assemble_transform(get_offset().z() * Vec3d::UnitZ(), rotation, get_scaling_factor(), get_mirror()); +// Vec3d rotation = get_rotation(); +// rotation.z() = 0.; +// Transform3d trafo_instance = Geometry::assemble_transform(get_offset().z() * Vec3d::UnitZ(), rotation, get_scaling_factor(), get_mirror()); - Polygon p = get_object()->convex_hull_2d(trafo_instance); + + Polygon p = get_object()->convex_hull_2d(this->get_matrix()); // if (!p.points.empty()) { // Polygons pp{p}; @@ -2420,8 +2421,8 @@ arrangement::ArrangePolygon ModelInstance::get_arrange_polygon() const arrangement::ArrangePolygon ret; ret.poly.contour = std::move(p); - ret.translation = Vec2crd{scaled(get_offset(X)), scaled(get_offset(Y))}; - ret.rotation = get_rotation(Z); + ret.translation = Vec2crd::Zero(); //Vec2crd{scaled(get_offset(X)), scaled(get_offset(Y))}; + ret.rotation = 0.; return ret; } diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 55aceeeb2..d29ce3525 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1170,9 +1170,16 @@ public: void apply_arrange_result(const Vec2d& offs, double rotation) { // write the transformation data into the model instance - set_rotation(Z, rotation); - set_offset(X, unscale(offs(X))); - set_offset(Y, unscale(offs(Y))); +// set_rotation(Z, rotation); +// set_offset(X, unscale(offs(X))); +// set_offset(Y, unscale(offs(Y))); + auto trafo = get_transformation().get_matrix(); + trafo.translate(to_3d(unscaled(offs), 0.)); + trafo.rotate(Eigen::AngleAxisd(rotation, Vec3d::UnitZ())); + m_transformation.set_matrix(trafo); + +// set_rotation(Z, get_rotation().z() + rotation); +// set_offset(get_offset() + to_3d(unscaled(offs), 0.)); this->object->invalidate_bounding_box(); } From 362267431be2d271ef0ccd13cf4abf673cf9a51f Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Tue, 4 Apr 2023 14:39:45 +0200 Subject: [PATCH 088/115] Fix transformation assembly problem for arrange polyogon --- src/libslic3r/Model.cpp | 19 +++++++++++++------ src/libslic3r/Model.hpp | 16 +--------------- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index f0ad7e1e0..837f32479 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2405,11 +2405,6 @@ void ModelInstance::transform_polygon(Polygon* polygon) const arrangement::ArrangePolygon ModelInstance::get_arrange_polygon() const { // static const double SIMPLIFY_TOLERANCE_MM = 0.1; - -// Vec3d rotation = get_rotation(); -// rotation.z() = 0.; -// Transform3d trafo_instance = Geometry::assemble_transform(get_offset().z() * Vec3d::UnitZ(), rotation, get_scaling_factor(), get_mirror()); - Polygon p = get_object()->convex_hull_2d(this->get_matrix()); @@ -2421,12 +2416,24 @@ arrangement::ArrangePolygon ModelInstance::get_arrange_polygon() const arrangement::ArrangePolygon ret; ret.poly.contour = std::move(p); - ret.translation = Vec2crd::Zero(); //Vec2crd{scaled(get_offset(X)), scaled(get_offset(Y))}; + ret.translation = Vec2crd::Zero(); ret.rotation = 0.; return ret; } +void ModelInstance::apply_arrange_result(const Vec2d &offs, double rotation) +{ + // write the transformation data into the model instance + auto trafo = get_transformation().get_matrix(); + auto tr = Transform3d::Identity(); + tr.translate(to_3d(unscaled(offs), 0.)); + trafo = tr * Eigen::AngleAxisd(rotation, Vec3d::UnitZ()) * trafo; + m_transformation.set_matrix(trafo); + + this->object->invalidate_bounding_box(); +} + indexed_triangle_set FacetsAnnotation::get_facets(const ModelVolume& mv, EnforcerBlockerType type) const { TriangleSelector selector(mv.mesh()); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index d29ce3525..ea22b968d 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1167,21 +1167,7 @@ public: arrangement::ArrangePolygon get_arrange_polygon() const; // Apply the arrange result on the ModelInstance - void apply_arrange_result(const Vec2d& offs, double rotation) - { - // write the transformation data into the model instance -// set_rotation(Z, rotation); -// set_offset(X, unscale(offs(X))); -// set_offset(Y, unscale(offs(Y))); - auto trafo = get_transformation().get_matrix(); - trafo.translate(to_3d(unscaled(offs), 0.)); - trafo.rotate(Eigen::AngleAxisd(rotation, Vec3d::UnitZ())); - m_transformation.set_matrix(trafo); - -// set_rotation(Z, get_rotation().z() + rotation); -// set_offset(get_offset() + to_3d(unscaled(offs), 0.)); - this->object->invalidate_bounding_box(); - } + void apply_arrange_result(const Vec2d& offs, double rotation); protected: friend class Print; From 49fbf4ccceaf71ec130f49e9a9fe2c3d4208ccb3 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 3 May 2023 09:45:11 +0200 Subject: [PATCH 089/115] Avoid garbage error message when generating sla preview --- src/libslic3r/SLAPrintSteps.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/SLAPrintSteps.cpp b/src/libslic3r/SLAPrintSteps.cpp index a38b17da1..33e2a6e20 100644 --- a/src/libslic3r/SLAPrintSteps.cpp +++ b/src/libslic3r/SLAPrintSteps.cpp @@ -305,7 +305,7 @@ void SLAPrint::Steps::generate_preview(SLAPrintObject &po, SLAPrintObjectStep st bench.stop(); - if (!m.empty()) + if (!po.m_preview_meshes[step]->empty()) BOOST_LOG_TRIVIAL(trace) << "Preview gen took: " << bench.getElapsedSec(); else BOOST_LOG_TRIVIAL(error) << "Preview failed!"; From 234956dfda61d9669d7a16b3957cd3de277c525c Mon Sep 17 00:00:00 2001 From: YuSanka Date: Wed, 3 May 2023 14:26:06 +0200 Subject: [PATCH 090/115] Fix for SPE-1686 : Bug with instance rotation around X or Y Steps to repro: 1. Add object 2. Increase instances 3. Select some instance 4. Rotate around X or Y 5. After MouseUp: OSX -> hard crash MSW and Linux -> object and instance are selected at a same time --- src/slic3r/GUI/GLCanvas3D.cpp | 30 +++++++++++++++++++++++++----- src/slic3r/GUI/GUI_ObjectList.cpp | 24 +++++++++++------------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index dbb210680..8116e8843 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -3641,6 +3641,7 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) } // Fixes flying instances + std::set obj_idx_for_update_info_items; for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; const double shift_z = m->get_instance_min_z(i.second); @@ -3649,8 +3650,11 @@ void GLCanvas3D::do_move(const std::string& snapshot_type) m_selection.translate(i.first, i.second, shift); m->translate_instance(i.second, shift); } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + obj_idx_for_update_info_items.emplace(i.first); } + //update sinking information in ObjectList + for (int id : obj_idx_for_update_info_items) + wxGetApp().obj_list()->update_info_items(static_cast(id)); // if the selection is not valid to allow for layer editing after the move, we need to turn off the tool if it is running // similar to void Plater::priv::selection_changed() @@ -3728,6 +3732,7 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) } // Fixes sinking/flying instances + std::set obj_idx_for_update_info_items; for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; const double shift_z = m->get_instance_min_z(i.second); @@ -3738,8 +3743,11 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type) m->translate_instance(i.second, shift); } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + obj_idx_for_update_info_items.emplace(i.first); } + //update sinking information in ObjectList + for (int id : obj_idx_for_update_info_items) + wxGetApp().obj_list()->update_info_items(static_cast(id)); if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED)); @@ -3797,6 +3805,7 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) } // Fixes sinking/flying instances + std::set obj_idx_for_update_info_items; for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; const double shift_z = m->get_instance_min_z(i.second); @@ -3806,8 +3815,11 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type) m_selection.translate(i.first, i.second, shift); m->translate_instance(i.second, shift); } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + obj_idx_for_update_info_items.emplace(i.first); } + //update sinking information in ObjectList + for (int id : obj_idx_for_update_info_items) + wxGetApp().obj_list()->update_info_items(static_cast(id)); if (!done.empty()) post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED)); @@ -3860,6 +3872,7 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) } // Fixes sinking/flying instances + std::set obj_idx_for_update_info_items; for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; double shift_z = m->get_instance_min_z(i.second); @@ -3869,8 +3882,11 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type) m_selection.translate(i.first, i.second, shift); m->translate_instance(i.second, shift); } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + obj_idx_for_update_info_items.emplace(i.first); } + //update sinking information in ObjectList + for (int id : obj_idx_for_update_info_items) + wxGetApp().obj_list()->update_info_items(static_cast(id)); post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); @@ -3922,6 +3938,7 @@ void GLCanvas3D::do_reset_skew(const std::string& snapshot_type) } // Fixes sinking/flying instances + std::set obj_idx_for_update_info_items; for (const std::pair& i : done) { ModelObject* m = m_model->objects[i.first]; double shift_z = m->get_instance_min_z(i.second); @@ -3931,8 +3948,11 @@ void GLCanvas3D::do_reset_skew(const std::string& snapshot_type) m_selection.translate(i.first, i.second, shift); m->translate_instance(i.second, shift); } - wxGetApp().obj_list()->update_info_items(static_cast(i.first)); + obj_idx_for_update_info_items.emplace(i.first); } + //update sinking information in ObjectList + for (int id : obj_idx_for_update_info_items) + wxGetApp().obj_list()->update_info_items(static_cast(id)); post_event(SimpleEvent(EVT_GLCANVAS_RESET_SKEW)); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 85d12bec9..ffed1eef6 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -2994,21 +2994,19 @@ void ObjectList::update_info_items(size_t obj_idx, wxDataViewItemArray* selectio wxGetApp().notification_manager()->push_updated_item_info_notification(type); } else if (shows && ! should_show) { - if (!selections) + if (!selections && IsSelected(item)) { Unselect(item); - m_objects_model->Delete(item); - if (selections) { - if (selections->Index(item) != wxNOT_FOUND) { - // If info item was deleted from the list, - // it's need to be deleted from selection array, if it was there - selections->Remove(item); - // Select item_obj, if info_item doesn't exist for item anymore, but was selected - if (selections->Index(item_obj) == wxNOT_FOUND) - selections->Add(item_obj); - } - } - else Select(item_obj); + } + m_objects_model->Delete(item); + if (selections && selections->Index(item) != wxNOT_FOUND) { + // If info item was deleted from the list, + // it's need to be deleted from selection array, if it was there + selections->Remove(item); + // Select item_obj, if info_item doesn't exist for item anymore, but was selected + if (selections->Index(item_obj) == wxNOT_FOUND) + selections->Add(item_obj); + } } } } From a039391131fc6cecbfbca406bd3a2627b93814cc Mon Sep 17 00:00:00 2001 From: Lukas Matena Date: Thu, 4 May 2023 13:56:05 +0200 Subject: [PATCH 091/115] Fixed rendering of horizontal ellipsis in ImGui controls --- src/slic3r/GUI/ImGuiWrapper.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index 0754c35cc..f2e6b0287 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -1620,6 +1620,8 @@ void ImGuiWrapper::init_font(bool compress) ImFontGlyphRangesBuilder builder; builder.AddRanges(m_glyph_ranges); + builder.AddChar(ImWchar(0x2026)); // … + if (m_font_cjk) { // This is a temporary fix of https://github.com/prusa3d/PrusaSlicer/issues/8171. The translation // contains characters not in the ImGui ranges for simplified Chinese. For now, just add them manually. From 34015349c177feef289dc3f480f5d52cc69ddecf Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 4 May 2023 14:59:50 +0200 Subject: [PATCH 092/115] Fix a rounded extrusion model when the new width is smaller than layer height - the code used radius in place of diameter --- src/libslic3r/Flow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Flow.cpp b/src/libslic3r/Flow.cpp index 1084e6f10..bc3a2d777 100644 --- a/src/libslic3r/Flow.cpp +++ b/src/libslic3r/Flow.cpp @@ -176,7 +176,7 @@ Flow Flow::with_cross_section(float area_new) const return this->with_width(width_new); } else { // Create a rounded extrusion. - auto dmr = float(sqrt(area_new / M_PI)); + auto dmr = 2.0 * float(sqrt(area_new / M_PI)); return Flow(dmr, dmr, m_spacing, m_nozzle_diameter, false); } } else From 8593ad1f80011994b33a130c2d634cb83acda5f6 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 4 May 2023 15:26:41 +0200 Subject: [PATCH 093/115] Organic supports improvements: Removing collisions with trees, limiting how far tree bottoms at slanted surfaces could be extended down below their last full circle position. Placable areas are now calculated sitting on slightly inflated top surface to indicate support of tree bottoms at slanted surfaces. --- src/libslic3r/ClipperUtils.cpp | 2 + src/libslic3r/ClipperUtils.hpp | 3 + src/libslic3r/TreeModelVolumes.cpp | 17 ++-- src/libslic3r/TreeModelVolumes.hpp | 3 +- src/libslic3r/TreeSupport.cpp | 156 ++++++++++++++++++++++------- src/libslic3r/TreeSupport.hpp | 12 +++ 6 files changed, 147 insertions(+), 46 deletions(-) diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index d10e14cc5..85ef53c88 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -685,6 +685,8 @@ Slic3r::Polygons diff(const Slic3r::Surfaces &subject, const Slic3r::Polygons &c { return _clipper(ClipperLib::ctDifference, ClipperUtils::SurfacesProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctIntersection, ClipperUtils::SinglePathProvider(subject.points), ClipperUtils::SinglePathProvider(clip.points), do_safety_offset); } +Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) + { return intersection(subject, ClipperUtils::clip_clipper_polygons_with_subject_bbox(clip, get_extents(subject).inflated(SCALED_EPSILON)), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset) { return _clipper(ClipperLib::ctIntersection, ClipperUtils::PolygonsProvider(subject), ClipperUtils::ExPolygonProvider(clip), do_safety_offset); } Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset) diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index ab967fd8f..774e9cb42 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -453,6 +453,9 @@ inline Slic3r::Lines diff_ln(const Slic3r::Lines &subject, const Slic3r::Polygon Slic3r::Polygons intersection(const Slic3r::Polygon &subject, const Slic3r::Polygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); +// Optimized version clipping the "clipping" polygon using clip_clipper_polygon_with_subject_bbox(). +// To be used with complex clipping polygons, where majority of the clipping polygons are outside of the source polygon. +Slic3r::Polygons intersection_clipped(const Slic3r::Polygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::ExPolygon &subject, const Slic3r::ExPolygon &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); Slic3r::Polygons intersection(const Slic3r::ExPolygons &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No); diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index 3c82b5c6c..10840d1b5 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -426,7 +426,7 @@ const Polygons& TreeModelVolumes::getPlaceableAreas(const coord_t orig_radius, L return (*result).get(); if (m_precalculated) { BOOST_LOG_TRIVIAL(error_level_not_in_cache) << "Had to calculate Placeable Areas at radius " << radius << " and layer " << layer_idx << ", but precalculate was called. Performance may suffer!"; - tree_supports_show_error("Not precalculated Placeable areas requested."sv, false); + tree_supports_show_error(format("Not precalculated Placeable areas requested, radius %1%, layer %2%", radius, layer_idx), false); } if (orig_radius == 0) // Placable areas for radius 0 are calculated in the general collision code. @@ -597,16 +597,19 @@ void TreeModelVolumes::calculateCollision(const coord_t radius, const LayerIndex // 3) Optionally calculate placables. if (calculate_placable) { // Now calculate the placable areas. - tbb::parallel_for(tbb::blocked_range(std::max(data.idx_begin, 1), data.idx_end), - [&collision_areas_offsetted, &anti_overhang = m_anti_overhang, processing_last_mesh, - min_resolution = m_min_resolution, &data_placeable, &throw_on_cancel] + tbb::parallel_for(tbb::blocked_range(std::max(z_distance_bottom_layers + 1, data.idx_begin), data.idx_end), + [&collision_areas_offsetted, &outlines, &anti_overhang = m_anti_overhang, processing_last_mesh, + min_resolution = m_min_resolution, z_distance_bottom_layers, xy_distance, &data_placeable, &throw_on_cancel] (const tbb::blocked_range& range) { for (LayerIndex layer_idx = range.begin(); layer_idx != range.end(); ++ layer_idx) { - LayerIndex layer_idx_below = layer_idx - 1; + LayerIndex layer_idx_below = layer_idx - z_distance_bottom_layers - 1; assert(layer_idx_below >= 0); const Polygons ¤t = collision_areas_offsetted[layer_idx]; - const Polygons &below = collision_areas_offsetted[layer_idx_below]; - Polygons placable = diff(below, layer_idx_below < int(anti_overhang.size()) ? union_(current, anti_overhang[layer_idx_below]) : current); + const Polygons &below = outlines[layer_idx_below]; + Polygons placable = diff( + // Inflate the surface to sit on by the separation distance to increase chance of a support being placed on a sloped surface. + offset(below, xy_distance), + layer_idx_below < int(anti_overhang.size()) ? union_(current, anti_overhang[layer_idx_below]) : current); auto &dst = data_placeable[layer_idx]; if (processing_last_mesh) { if (! dst.empty()) diff --git a/src/libslic3r/TreeModelVolumes.hpp b/src/libslic3r/TreeModelVolumes.hpp index 139b12328..659baf1ff 100644 --- a/src/libslic3r/TreeModelVolumes.hpp +++ b/src/libslic3r/TreeModelVolumes.hpp @@ -215,6 +215,7 @@ public: void clear() { this->clear_all_but_object_collision(); m_collision_cache.clear(); + m_placeable_areas_cache.clear(); } void clear_all_but_object_collision() { //m_collision_cache.clear_all_but_radius0(); @@ -223,7 +224,7 @@ public: m_avoidance_cache_slow.clear(); m_avoidance_cache_to_model.clear(); m_avoidance_cache_to_model_slow.clear(); - m_placeable_areas_cache.clear(); + m_placeable_areas_cache.clear_all_but_radius0(); m_avoidance_cache_holefree.clear(); m_avoidance_cache_holefree_to_model.clear(); m_wall_restrictions_cache.clear(); diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 820b60d49..9a7b203c6 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -299,7 +299,7 @@ static bool inline g_showed_critical_error = false; static bool inline g_showed_performance_warning = false; void tree_supports_show_error(std::string_view message, bool critical) { // todo Remove! ONLY FOR PUBLIC BETA!! - +// printf("Error: %s, critical: %d\n", message.data(), int(critical)); #ifdef TREE_SUPPORT_SHOW_ERRORS_WIN32 static bool showed_critical = false; static bool showed_performance = false; @@ -2158,6 +2158,10 @@ static void increase_areas_one_layer( " Distance to top: " << parent.state.distance_to_top << " Elephant foot increases " << parent.state.elephant_foot_increases << " use_min_xy_dist " << parent.state.use_min_xy_dist << " to buildplate " << parent.state.to_buildplate << " gracious " << parent.state.to_model_gracious << " safe " << parent.state.can_use_safe_radius << " until move " << parent.state.dont_move_until; tree_supports_show_error("Potentially lost branch!"sv, true); +#ifdef TREE_SUPPORTS_TRACK_LOST + if (result) + result->lost = true; +#endif // TREE_SUPPORTS_TRACK_LOST } else result = increase_single_area(volumes, config, settings, layer_idx, parent, settings.increase_speed == slow_speed ? offset_slow : offset_fast, to_bp_data, to_model_data, inc_wo_collision, 0, mergelayer); @@ -2211,6 +2215,9 @@ static void increase_areas_one_layer( // But as branches connecting with the model that are to small have to be culled, the bottom most point has to be not set. // A point can be set on the top most tip layer (maybe more if it should not move for a few layers). parent.state.result_on_layer_reset(); +#ifdef TREE_SUPPORTS_TRACK_LOST + parent.state.verylost = true; +#endif // TREE_SUPPORTS_TRACK_LOST } throw_on_cancel(); @@ -4234,26 +4241,35 @@ static std::vector draw_branches( branch.path.emplace_back(&start_element); // Traverse each branch until it branches again. SupportElement &first_parent = layer_above[start_element.parents[parent_idx]]; + assert(! first_parent.state.marked); assert(branch.path.back()->state.layer_idx + 1 == first_parent.state.layer_idx); branch.path.emplace_back(&first_parent); if (first_parent.parents.size() < 2) first_parent.state.marked = true; SupportElement *next_branch = nullptr; - if (first_parent.parents.size() == 1) + if (first_parent.parents.size() == 1) { for (SupportElement *parent = &first_parent;;) { + assert(parent->state.marked); SupportElement &next_parent = move_bounds[parent->state.layer_idx + 1][parent->parents.front()]; + assert(! next_parent.state.marked); assert(branch.path.back()->state.layer_idx + 1 == next_parent.state.layer_idx); branch.path.emplace_back(&next_parent); if (next_parent.parents.size() > 1) { + // Branching point was reached. next_branch = &next_parent; break; } next_parent.state.marked = true; if (next_parent.parents.size() == 0) + // Tip is reached. break; parent = &next_parent; } + } else if (first_parent.parents.size() > 1) + // Branching point was reached. + next_branch = &first_parent; assert(branch.path.size() >= 2); + assert(next_branch == nullptr || ! next_branch->state.marked); branch.has_root = root; branch.has_tip = ! next_branch; out.branches.emplace_back(std::move(branch)); @@ -4263,13 +4279,37 @@ static std::vector draw_branches( } }; - for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) - for (SupportElement &start_element : move_bounds[layer_idx]) - if (! start_element.state.marked && ! start_element.parents.empty()) { + for (LayerIndex layer_idx = 0; layer_idx + 1 < LayerIndex(move_bounds.size()); ++ layer_idx) { +// int ielement; + for (SupportElement& start_element : move_bounds[layer_idx]) { + if (!start_element.state.marked && !start_element.parents.empty()) { +#if 0 + int found = 0; + if (layer_idx > 0) { + for (auto& el : move_bounds[layer_idx - 1]) { + for (auto iparent : el.parents) + if (iparent == ielement) + ++found; + } + if (found != 0) + printf("Found: %d\n", found); + } +#endif trees.push_back({}); TreeVisitor::visit_recursive(move_bounds, start_element, trees.back()); - assert(! trees.back().branches.empty()); + assert(!trees.back().branches.empty()); + //FIXME debugging +#if 0 + if (start_element.state.lost) { + } + else if (start_element.state.verylost) { + } else + trees.pop_back(); +#endif } +// ++ ielement; + } + } const SlicingParameters &slicing_params = print_object.slicing_parameters(); MeshSlicingParams mesh_slicing_params; @@ -4297,49 +4337,89 @@ static std::vector draw_branches( slice_z.emplace_back(float(0.5 * (bottom_z + print_z))); } std::vector slices = slice_mesh(partial_mesh, slice_z, mesh_slicing_params, throw_on_cancel); - size_t num_empty = 0; + //FIXME parallelize? + for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++ i) + slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); //FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); + size_t num_empty = 0; if (layer_begin > 0 && branch.has_root && ! branch.path.front()->state.to_model_gracious && ! slices.front().empty()) { // Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something. - std::vector bottom_extra_slices; - Polygons rest_support; - for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= 0; -- layer_idx) { + struct BottomExtraSlice { + Polygons polygons; + Polygons supported; + double area; + double supported_area; + }; + std::vector bottom_extra_slices; + Polygons rest_support; + coord_t bottom_radius = config.getRadius(branch.path.front()->state); + // Don't propagate further than 1.5 * bottom radius. + //LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height; + LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height; + LayerIndex layer_bottommost = std::max(0, layer_begin - layers_propagate_max); + // Only propagate until the rest area is smaller than this threshold. + double support_area_stop = 0.2 * M_PI * sqr(double(bottom_radius)); + // Only propagate until the rest area is smaller than this threshold. + double support_area_min = 0.1 * M_PI * sqr(double(config.min_radius)); + for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); - if (area(rest_support) < tiny_area_threshold) + double rest_support_area = area(rest_support); + if (rest_support_area < support_area_stop) + // Don't propagate a fraction of the tree contact surface. break; - bottom_extra_slices.emplace_back(rest_support); + // Measure how much the rest_support is actually supported. + /* + Polygons supported = intersection_clipped(rest_support, volumes.getPlaceableAreas(0, layer_idx, []{})); + double supported_area = area(supported); + printf("Supported area: %d, %lf\n", layer_idx, supported_area); + */ + Polygons supported; + double supported_area; + bottom_extra_slices.push_back({ rest_support, std::move(supported), rest_support_area, supported_area }); } + // Now remove those bottom slices that are not supported at all. + while (! bottom_extra_slices.empty() && + area(intersection_clipped(bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {}))) < support_area_min) + bottom_extra_slices.pop_back(); layer_begin -= LayerIndex(bottom_extra_slices.size()); - slices.insert(slices.begin(), std::make_move_iterator(bottom_extra_slices.rbegin()), std::make_move_iterator(bottom_extra_slices.rend())); + slices.insert(slices.begin(), bottom_extra_slices.size(), {}); + size_t i = 0; + for (auto it = bottom_extra_slices.rbegin(); it != bottom_extra_slices.rend(); ++it, ++i) + slices[i] = std::move(it->polygons); } else num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); layer_begin += LayerIndex(num_empty); - for (; slices.back().empty(); -- layer_end); - LayerIndex new_begin = tree.first_layer_id == -1 ? layer_begin : std::min(tree.first_layer_id, layer_begin); - LayerIndex new_end = tree.first_layer_id == -1 ? layer_end : std::max(tree.first_layer_id + LayerIndex(tree.slices.size()), layer_end); - size_t new_size = size_t(new_end - new_begin); - if (tree.first_layer_id == -1) { - } else if (tree.slices.capacity() < new_size) { - std::vector new_slices; - new_slices.reserve(new_size); - if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) - new_slices.insert(new_slices.end(), dif, {}); - append(new_slices, std::move(tree.slices)); - tree.slices.swap(new_slices); - } else if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) - tree.slices.insert(tree.slices.begin(), tree.first_layer_id - new_begin, {}); - tree.slices.insert(tree.slices.end(), new_size - tree.slices.size(), {}); - layer_begin -= LayerIndex(num_empty); - for (LayerIndex i = layer_begin; i != layer_end; ++ i) - if (Polygons &src = slices[i - layer_begin]; ! src.empty()) { - Slice &dst = tree.slices[i - new_begin]; - if (++ dst.num_branches > 1) - append(dst.polygons, std::move(src)); - else - dst.polygons = std::move(std::move(src)); - } - tree.first_layer_id = new_begin; + while (! slices.empty() && slices.back().empty()) { + slices.pop_back(); + -- layer_end; + } + if (layer_begin < layer_end) { + LayerIndex new_begin = tree.first_layer_id == -1 ? layer_begin : std::min(tree.first_layer_id, layer_begin); + LayerIndex new_end = tree.first_layer_id == -1 ? layer_end : std::max(tree.first_layer_id + LayerIndex(tree.slices.size()), layer_end); + size_t new_size = size_t(new_end - new_begin); + if (tree.first_layer_id == -1) { + } else if (tree.slices.capacity() < new_size) { + std::vector new_slices; + new_slices.reserve(new_size); + if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) + new_slices.insert(new_slices.end(), dif, {}); + append(new_slices, std::move(tree.slices)); + tree.slices.swap(new_slices); + } else if (LayerIndex dif = tree.first_layer_id - new_begin; dif > 0) + tree.slices.insert(tree.slices.begin(), tree.first_layer_id - new_begin, {}); + tree.slices.insert(tree.slices.end(), new_size - tree.slices.size(), {}); + layer_begin -= LayerIndex(num_empty); + for (LayerIndex i = layer_begin; i != layer_end; ++ i) + if (Polygons &src = slices[i - layer_begin]; ! src.empty()) { + Slice &dst = tree.slices[i - new_begin]; + if (++ dst.num_branches > 1) + append(dst.polygons, std::move(src)); + else + dst.polygons = std::move(std::move(src)); + } + tree.first_layer_id = new_begin; + } } } }, tbb::simple_partitioner()); diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/TreeSupport.hpp index 070903096..2c87a132d 100644 --- a/src/libslic3r/TreeSupport.hpp +++ b/src/libslic3r/TreeSupport.hpp @@ -93,6 +93,8 @@ struct AreaIncreaseSettings struct TreeSupportSettings; +// #define TREE_SUPPORTS_TRACK_LOST + // C++17 does not support in place initializers of bit values, thus a constructor zeroing the bits is provided. struct SupportElementStateBits { SupportElementStateBits() : @@ -102,6 +104,10 @@ struct SupportElementStateBits { supports_roof(false), can_use_safe_radius(false), skip_ovalisation(false), +#ifdef TREE_SUPPORTS_TRACK_LOST + lost(false), + verylost(false), +#endif // TREE_SUPPORTS_TRACK_LOST deleted(false), marked(false) {} @@ -136,6 +142,12 @@ struct SupportElementStateBits { */ bool skip_ovalisation : 1; +#ifdef TREE_SUPPORTS_TRACK_LOST + // Likely a lost branch, debugging information. + bool lost : 1; + bool verylost : 1; +#endif // TREE_SUPPORTS_TRACK_LOST + // Not valid anymore, to be deleted. bool deleted : 1; From 3b9037b44248ca19a49545ed31933c1b4c31bfe4 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 4 May 2023 15:27:09 +0200 Subject: [PATCH 094/115] nlopt in RelWithDebInfo now compiles as release. --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 66b9a777b..49fe3437f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -546,6 +546,7 @@ foreach(po_file ${L10N_PO_FILES}) endforeach() find_package(NLopt 1.4 REQUIRED) +slic3r_remap_configs(NLopt::nlopt RelWithDebInfo Release) if(SLIC3R_STATIC) set(OPENVDB_USE_STATIC_LIBS ON) From 6d0ceeb8861bfda4c3d84ffd1ea1c3fc700d406d Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 12:59:20 +0200 Subject: [PATCH 095/115] ClipperLib: Further optimization of memory allocation using scalable_allocator. ClipperLib: SimplifyPolygon() - changed default winding number to positive, added strictly_simple parameter. ClipperUtlis simplify_polygons() - removed "remove_collinear" parameter --- src/clipper/clipper.cpp | 206 +++++++++++------------- src/clipper/clipper.hpp | 43 +++-- src/libslic3r/Arachne/WallToolPaths.cpp | 4 +- src/libslic3r/ClipperUtils.cpp | 25 ++- src/libslic3r/ClipperUtils.hpp | 4 +- 5 files changed, 137 insertions(+), 145 deletions(-) diff --git a/src/clipper/clipper.cpp b/src/clipper/clipper.cpp index c775a3226..5da79d3a1 100644 --- a/src/clipper/clipper.cpp +++ b/src/clipper/clipper.cpp @@ -73,25 +73,6 @@ static int const Skip = -2; //edge that would otherwise close a path #define TOLERANCE (1.0e-20) #define NEAR_ZERO(val) (((val) > -TOLERANCE) && ((val) < TOLERANCE)) -// Output polygon. -struct OutRec { - int Idx; - bool IsHole; - bool IsOpen; - //The 'FirstLeft' field points to another OutRec that contains or is the - //'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is - //parsed left from the current edge (owning OutRec) until the owner OutRec - //is found. This field simplifies sorting the polygons into a tree structure - //which reflects the parent/child relationships of all polygons. - //This field should be renamed Parent, and will be later. - OutRec *FirstLeft; - // Used only by void Clipper::BuildResult2(PolyTree& polytree) - PolyNode *PolyNd; - // Linked list of output points, dynamically allocated. - OutPt *Pts; - OutPt *BottomPt; -}; - //------------------------------------------------------------------------------ inline IntPoint IntPoint2d(cInt x, cInt y) @@ -1061,8 +1042,7 @@ IntRect ClipperBase::GetBounds() Clipper::Clipper(int initOptions) : ClipperBase(), m_OutPtsFree(nullptr), - m_OutPtsChunkSize(32), - m_OutPtsChunkLast(32), + m_OutPtsChunkLast(m_OutPtsChunkSize), m_ActiveEdges(nullptr), m_SortedEdges(nullptr) { @@ -1153,23 +1133,23 @@ bool Clipper::ExecuteInternal() //FIXME Vojtech: Does it not invalidate the loop hierarchy maintained as OutRec::FirstLeft pointers? //FIXME Vojtech: The area is calculated with floats, it may not be numerically stable! { - for (OutRec *outRec : m_PolyOuts) - if (outRec->Pts && !outRec->IsOpen && (outRec->IsHole ^ m_ReverseOutput) == (Area(*outRec) > 0)) - ReversePolyPtLinks(outRec->Pts); + for (OutRec &outRec : m_PolyOuts) + if (outRec.Pts && !outRec.IsOpen && (outRec.IsHole ^ m_ReverseOutput) == (Area(outRec) > 0)) + ReversePolyPtLinks(outRec.Pts); } JoinCommonEdges(); //unfortunately FixupOutPolygon() must be done after JoinCommonEdges() { - for (OutRec *outRec : m_PolyOuts) - if (outRec->Pts) { - if (outRec->IsOpen) + for (OutRec &outRec : m_PolyOuts) + if (outRec.Pts) { + if (outRec.IsOpen) // Removes duplicate points. - FixupOutPolyline(*outRec); + FixupOutPolyline(outRec); else // Removes duplicate points and simplifies consecutive parallel edges by removing the middle vertex. - FixupOutPolygon(*outRec); + FixupOutPolygon(outRec); } } // For each polygon, search for exactly duplicate non-successive points. @@ -1194,22 +1174,18 @@ OutPt* Clipper::AllocateOutPt() m_OutPtsFree = pt->Next; } else if (m_OutPtsChunkLast < m_OutPtsChunkSize) { // Get a point from the last chunk. - pt = m_OutPts.back() + (m_OutPtsChunkLast ++); + pt = &m_OutPts.back()[m_OutPtsChunkLast ++]; } else { // The last chunk is full. Allocate a new one. - m_OutPts.push_back(new OutPt[m_OutPtsChunkSize]); + m_OutPts.push_back({}); m_OutPtsChunkLast = 1; - pt = m_OutPts.back(); + pt = &m_OutPts.back().front(); } return pt; } void Clipper::DisposeAllOutRecs() { - for (OutPt *pts : m_OutPts) - delete[] pts; - for (OutRec *rec : m_PolyOuts) - delete rec; m_OutPts.clear(); m_OutPtsFree = nullptr; m_OutPtsChunkLast = m_OutPtsChunkSize; @@ -1832,7 +1808,7 @@ void Clipper::IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &Pt) } //------------------------------------------------------------------------------ -void Clipper::SetHoleState(TEdge *e, OutRec *outrec) const +void Clipper::SetHoleState(TEdge *e, OutRec *outrec) { bool IsHole = false; TEdge *e2 = e->PrevInAEL; @@ -1842,7 +1818,7 @@ void Clipper::SetHoleState(TEdge *e, OutRec *outrec) const { IsHole = !IsHole; if (! outrec->FirstLeft) - outrec->FirstLeft = m_PolyOuts[e2->OutIdx]; + outrec->FirstLeft = &m_PolyOuts[e2->OutIdx]; } e2 = e2->PrevInAEL; } @@ -1883,18 +1859,18 @@ bool Param1RightOfParam2(OutRec* outRec1, OutRec* outRec2) OutRec* Clipper::GetOutRec(int Idx) { - OutRec* outrec = m_PolyOuts[Idx]; - while (outrec != m_PolyOuts[outrec->Idx]) - outrec = m_PolyOuts[outrec->Idx]; + OutRec* outrec = &m_PolyOuts[Idx]; + while (outrec != &m_PolyOuts[outrec->Idx]) + outrec = &m_PolyOuts[outrec->Idx]; return outrec; } //------------------------------------------------------------------------------ -void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) const +void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) { //get the start and ends of both output polygons ... - OutRec *outRec1 = m_PolyOuts[e1->OutIdx]; - OutRec *outRec2 = m_PolyOuts[e2->OutIdx]; + OutRec *outRec1 = &m_PolyOuts[e1->OutIdx]; + OutRec *outRec2 = &m_PolyOuts[e2->OutIdx]; OutRec *holeStateRec; if (Param1RightOfParam2(outRec1, outRec2)) @@ -1991,16 +1967,16 @@ void Clipper::AppendPolygon(TEdge *e1, TEdge *e2) const OutRec* Clipper::CreateOutRec() { - OutRec* result = new OutRec; - result->IsHole = false; - result->IsOpen = false; - result->FirstLeft = 0; - result->Pts = 0; - result->BottomPt = 0; - result->PolyNd = 0; - m_PolyOuts.push_back(result); - result->Idx = (int)m_PolyOuts.size()-1; - return result; + m_PolyOuts.push_back({}); + OutRec &result = m_PolyOuts.back(); + result.IsHole = false; + result.IsOpen = false; + result.FirstLeft = 0; + result.Pts = 0; + result.BottomPt = 0; + result.PolyNd = 0; + result.Idx = (int)m_PolyOuts.size()-1; + return &result; } //------------------------------------------------------------------------------ @@ -2022,7 +1998,7 @@ OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) return newOp; } else { - OutRec *outRec = m_PolyOuts[e->OutIdx]; + OutRec *outRec = &m_PolyOuts[e->OutIdx]; //OutRec.Pts is the 'Left-most' point & OutRec.Pts.Prev is the 'Right-most' OutPt* op = outRec->Pts; @@ -2045,7 +2021,7 @@ OutPt* Clipper::AddOutPt(TEdge *e, const IntPoint &pt) OutPt* Clipper::GetLastOutPt(TEdge *e) { - OutRec *outRec = m_PolyOuts[e->OutIdx]; + OutRec *outRec = &m_PolyOuts[e->OutIdx]; if (e->Side == esLeft) return outRec->Pts; else @@ -2216,7 +2192,7 @@ void Clipper::ProcessHorizontal(TEdge *horzEdge) { Direction dir; cInt horzLeft, horzRight; - bool IsOpen = (horzEdge->OutIdx >= 0 && m_PolyOuts[horzEdge->OutIdx]->IsOpen); + bool IsOpen = (horzEdge->OutIdx >= 0 && m_PolyOuts[horzEdge->OutIdx].IsOpen); GetHorzDirection(*horzEdge, dir, horzLeft, horzRight); @@ -2778,12 +2754,12 @@ int PointCount(OutPt *Pts) void Clipper::BuildResult(Paths &polys) { polys.reserve(m_PolyOuts.size()); - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - assert(! outRec->IsOpen); - if (!outRec->Pts) continue; + assert(! outRec.IsOpen); + if (!outRec.Pts) continue; Path pg; - OutPt* p = outRec->Pts->Prev; + OutPt* p = outRec.Pts->Prev; int cnt = PointCount(p); if (cnt < 2) continue; pg.reserve(cnt); @@ -2802,31 +2778,31 @@ void Clipper::BuildResult2(PolyTree& polytree) polytree.Clear(); polytree.AllNodes.reserve(m_PolyOuts.size()); //add each output polygon/contour to polytree ... - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - int cnt = PointCount(outRec->Pts); - if ((outRec->IsOpen && cnt < 2) || (!outRec->IsOpen && cnt < 3)) + int cnt = PointCount(outRec.Pts); + if ((outRec.IsOpen && cnt < 2) || (!outRec.IsOpen && cnt < 3)) // Ignore an invalid output loop or a polyline. continue; //skip OutRecs that (a) contain outermost polygons or //(b) already have the correct owner/child linkage ... - if (outRec->FirstLeft && - (outRec->IsHole == outRec->FirstLeft->IsHole || ! outRec->FirstLeft->Pts)) { - OutRec* orfl = outRec->FirstLeft; - while (orfl && ((orfl->IsHole == outRec->IsHole) || !orfl->Pts)) + if (outRec.FirstLeft && + (outRec.IsHole == outRec.FirstLeft->IsHole || ! outRec.FirstLeft->Pts)) { + OutRec* orfl = outRec.FirstLeft; + while (orfl && ((orfl->IsHole == outRec.IsHole) || !orfl->Pts)) orfl = orfl->FirstLeft; - outRec->FirstLeft = orfl; + outRec.FirstLeft = orfl; } //nb: polytree takes ownership of all the PolyNodes polytree.AllNodes.emplace_back(PolyNode()); PolyNode* pn = &polytree.AllNodes.back(); - outRec->PolyNd = pn; + outRec.PolyNd = pn; pn->Parent = 0; pn->Index = 0; pn->Contour.reserve(cnt); - OutPt *op = outRec->Pts->Prev; + OutPt *op = outRec.Pts->Prev; for (int j = 0; j < cnt; j++) { pn->Contour.emplace_back(op->Pt); @@ -2836,18 +2812,18 @@ void Clipper::BuildResult2(PolyTree& polytree) //fixup PolyNode links etc ... polytree.Childs.reserve(m_PolyOuts.size()); - for (OutRec* outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - if (!outRec->PolyNd) continue; - if (outRec->IsOpen) + if (!outRec.PolyNd) continue; + if (outRec.IsOpen) { - outRec->PolyNd->m_IsOpen = true; - polytree.AddChild(*outRec->PolyNd); + outRec.PolyNd->m_IsOpen = true; + polytree.AddChild(*outRec.PolyNd); } - else if (outRec->FirstLeft && outRec->FirstLeft->PolyNd) - outRec->FirstLeft->PolyNd->AddChild(*outRec->PolyNd); + else if (outRec.FirstLeft && outRec.FirstLeft->PolyNd) + outRec.FirstLeft->PolyNd->AddChild(*outRec.PolyNd); else - polytree.AddChild(*outRec->PolyNd); + polytree.AddChild(*outRec.PolyNd); } } //------------------------------------------------------------------------------ @@ -3193,26 +3169,26 @@ bool Clipper::JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2) //---------------------------------------------------------------------- // This is potentially very expensive! O(n^3)! -void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) const +void Clipper::FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) { //tests if NewOutRec contains the polygon before reassigning FirstLeft - for (OutRec *outRec : m_PolyOuts) + for (OutRec &outRec : m_PolyOuts) { - if (!outRec->Pts || !outRec->FirstLeft) continue; - OutRec* firstLeft = outRec->FirstLeft; + if (!outRec.Pts || !outRec.FirstLeft) continue; + OutRec* firstLeft = outRec.FirstLeft; // Skip empty polygons. while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (firstLeft == OldOutRec && Poly2ContainsPoly1(outRec->Pts, NewOutRec->Pts)) - outRec->FirstLeft = NewOutRec; + if (firstLeft == OldOutRec && Poly2ContainsPoly1(outRec.Pts, NewOutRec->Pts)) + outRec.FirstLeft = NewOutRec; } } //---------------------------------------------------------------------- -void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) const +void Clipper::FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) { //reassigns FirstLeft WITHOUT testing if NewOutRec contains the polygon - for (OutRec *outRec : m_PolyOuts) - if (outRec->FirstLeft == OldOutRec) outRec->FirstLeft = NewOutRec; + for (OutRec &outRec : m_PolyOuts) + if (outRec.FirstLeft == OldOutRec) outRec.FirstLeft = NewOutRec; } //---------------------------------------------------------------------- @@ -3253,13 +3229,13 @@ void Clipper::JoinCommonEdges() if (m_UsingPolyTree) for (size_t j = 0; j < m_PolyOuts.size() - 1; j++) { - OutRec* oRec = m_PolyOuts[j]; - OutRec* firstLeft = oRec->FirstLeft; + OutRec &oRec = m_PolyOuts[j]; + OutRec* firstLeft = oRec.FirstLeft; while (firstLeft && !firstLeft->Pts) firstLeft = firstLeft->FirstLeft; - if (!oRec->Pts || firstLeft != outRec1 || - oRec->IsHole == outRec1->IsHole) continue; - if (Poly2ContainsPoly1(oRec->Pts, join.OutPt2)) - oRec->FirstLeft = outRec2; + if (!oRec.Pts || firstLeft != outRec1 || + oRec.IsHole == outRec1->IsHole) continue; + if (Poly2ContainsPoly1(oRec.Pts, join.OutPt2)) + oRec.FirstLeft = outRec2; } if (Poly2ContainsPoly1(outRec2->Pts, outRec1->Pts)) @@ -3771,13 +3747,13 @@ void Clipper::DoSimplePolygons() size_t i = 0; while (i < m_PolyOuts.size()) { - OutRec* outrec = m_PolyOuts[i++]; - OutPt* op = outrec->Pts; - if (!op || outrec->IsOpen) continue; + OutRec &outrec = m_PolyOuts[i++]; + OutPt* op = outrec.Pts; + if (!op || outrec.IsOpen) continue; do //for each Pt in Polygon until duplicate found do ... { OutPt* op2 = op->Next; - while (op2 != outrec->Pts) + while (op2 != outrec.Pts) { if ((op->Pt == op2->Pt) && op2->Next != op && op2->Prev != op) { @@ -3789,37 +3765,37 @@ void Clipper::DoSimplePolygons() op2->Prev = op3; op3->Next = op2; - outrec->Pts = op; + outrec.Pts = op; OutRec* outrec2 = CreateOutRec(); outrec2->Pts = op2; UpdateOutPtIdxs(*outrec2); - if (Poly2ContainsPoly1(outrec2->Pts, outrec->Pts)) + if (Poly2ContainsPoly1(outrec2->Pts, outrec.Pts)) { //OutRec2 is contained by OutRec1 ... - outrec2->IsHole = !outrec->IsHole; - outrec2->FirstLeft = outrec; + outrec2->IsHole = !outrec.IsHole; + outrec2->FirstLeft = &outrec; // For each m_PolyOuts, replace FirstLeft from outRec2 to outrec. - if (m_UsingPolyTree) FixupFirstLefts2(outrec2, outrec); + if (m_UsingPolyTree) FixupFirstLefts2(outrec2, &outrec); } else - if (Poly2ContainsPoly1(outrec->Pts, outrec2->Pts)) + if (Poly2ContainsPoly1(outrec.Pts, outrec2->Pts)) { //OutRec1 is contained by OutRec2 ... - outrec2->IsHole = outrec->IsHole; - outrec->IsHole = !outrec2->IsHole; - outrec2->FirstLeft = outrec->FirstLeft; - outrec->FirstLeft = outrec2; + outrec2->IsHole = outrec.IsHole; + outrec.IsHole = !outrec2->IsHole; + outrec2->FirstLeft = outrec.FirstLeft; + outrec.FirstLeft = outrec2; // For each m_PolyOuts, replace FirstLeft from outrec to outrec2. - if (m_UsingPolyTree) FixupFirstLefts2(outrec, outrec2); + if (m_UsingPolyTree) FixupFirstLefts2(&outrec, outrec2); } else { //the 2 polygons are separate ... - outrec2->IsHole = outrec->IsHole; - outrec2->FirstLeft = outrec->FirstLeft; + outrec2->IsHole = outrec.IsHole; + outrec2->FirstLeft = outrec.FirstLeft; // For each polygon of m_PolyOuts, replace FirstLeft from outrec to outrec2 if the polygon is inside outRec2. //FIXME This is potentially very expensive! O(n^3)! - if (m_UsingPolyTree) FixupFirstLefts1(outrec, outrec2); + if (m_UsingPolyTree) FixupFirstLefts1(&outrec, outrec2); } op2 = op; //ie get ready for the Next iteration } @@ -3827,7 +3803,7 @@ void Clipper::DoSimplePolygons() } op = op->Next; } - while (op != outrec->Pts); + while (op != outrec.Pts); } } //------------------------------------------------------------------------------ @@ -3845,10 +3821,10 @@ void ReversePaths(Paths& p) } //------------------------------------------------------------------------------ -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType) +Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType, bool strictly_simple /* = true */) { Clipper c; - c.StrictlySimple(true); + c.StrictlySimple(strictly_simple); c.AddPath(in_poly, ptSubject, true); Paths out; c.Execute(ctUnion, out, fillType, fillType); diff --git a/src/clipper/clipper.hpp b/src/clipper/clipper.hpp index d190d09b5..c88545454 100644 --- a/src/clipper/clipper.hpp +++ b/src/clipper/clipper.hpp @@ -52,6 +52,7 @@ //use_deprecated: Enables temporary support for the obsolete functions //#define use_deprecated +#include #include #include #include @@ -199,7 +200,8 @@ double Area(const Path &poly); inline bool Orientation(const Path &poly) { return Area(poly) >= 0; } int PointInPolygon(const IntPoint &pt, const Path &path); -Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftEvenOdd); +// Union with "strictly simple" fix enabled. +Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftNonZero, bool strictly_simple = true); void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415); void CleanPolygon(Path& poly, double distance = 1.415); @@ -284,7 +286,25 @@ enum EdgeSide { esLeft = 1, esRight = 2}; using OutPts = std::vector>; - struct OutRec; + // Output polygon. + struct OutRec { + int Idx; + bool IsHole; + bool IsOpen; + //The 'FirstLeft' field points to another OutRec that contains or is the + //'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is + //parsed left from the current edge (owning OutRec) until the owner OutRec + //is found. This field simplifies sorting the polygons into a tree structure + //which reflects the parent/child relationships of all polygons. + //This field should be renamed Parent, and will be later. + OutRec* FirstLeft; + // Used only by void Clipper::BuildResult2(PolyTree& polytree) + PolyNode* PolyNd; + // Linked list of output points, dynamically allocated. + OutPt* Pts; + OutPt* BottomPt; + }; + struct Join { Join(OutPt *OutPt1, OutPt *OutPt2, IntPoint OffPt) : OutPt1(OutPt1), OutPt2(OutPt2), OffPt(OffPt) {} @@ -432,12 +452,12 @@ protected: private: // Output polygons. - std::vector> m_PolyOuts; + std::deque> m_PolyOuts; // Output points, allocated by a continuous sets of m_OutPtsChunkSize. - std::vector> m_OutPts; + static constexpr const size_t m_OutPtsChunkSize = 32; + std::deque, Allocator>> m_OutPts; // List of free output points, to be used before taking a point from m_OutPts or allocating a new chunk. OutPt *m_OutPtsFree; - size_t m_OutPtsChunkSize; size_t m_OutPtsChunkLast; std::vector> m_Joins; @@ -482,7 +502,7 @@ private: void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt); OutRec* GetOutRec(int idx); - void AppendPolygon(TEdge *e1, TEdge *e2) const; + void AppendPolygon(TEdge *e1, TEdge *e2); void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt); OutRec* CreateOutRec(); OutPt* AddOutPt(TEdge *e, const IntPoint &pt); @@ -498,7 +518,7 @@ private: void ProcessEdgesAtTopOfScanbeam(const cInt topY); void BuildResult(Paths& polys); void BuildResult2(PolyTree& polytree); - void SetHoleState(TEdge *e, OutRec *outrec) const; + void SetHoleState(TEdge *e, OutRec *outrec); bool FixupIntersectionOrder(); void FixupOutPolygon(OutRec &outrec); void FixupOutPolyline(OutRec &outrec); @@ -508,8 +528,8 @@ private: bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, const IntPoint &Pt, bool DiscardLeft); void JoinCommonEdges(); void DoSimplePolygons(); - void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec) const; - void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec) const; + void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec); + void FixupFirstLefts2(OutRec* OldOutRec, OutRec* NewOutRec); #ifdef CLIPPERLIB_USE_XYZ void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2); #endif @@ -567,10 +587,11 @@ class clipperException : public std::exception }; //------------------------------------------------------------------------------ +// Union with "strictly simple" fix enabled. template -inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftEvenOdd) { +inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftNonZero, bool strictly_simple = true) { Clipper c; - c.StrictlySimple(true); + c.StrictlySimple(strictly_simple); c.AddPaths(std::forward(in_polys), ptSubject, true); Paths out; c.Execute(ctUnion, out, fillType, fillType); diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index fce69d5e4..6c5dafdac 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -232,7 +232,7 @@ std::unique_ptr cre void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) { if (epsilon < 1) { - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); return; } @@ -273,7 +273,7 @@ void fixSelfIntersections(const coord_t epsilon, Polygons &thiss) } } - ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss)); + ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(thiss), ClipperLib::pftEvenOdd); } /*! diff --git a/src/libslic3r/ClipperUtils.cpp b/src/libslic3r/ClipperUtils.cpp index 95a533718..d10e14cc5 100644 --- a/src/libslic3r/ClipperUtils.cpp +++ b/src/libslic3r/ClipperUtils.cpp @@ -964,21 +964,18 @@ Polygons union_pt_chained_outside_in(const Polygons &subject) return retval; } -Polygons simplify_polygons(const Polygons &subject, bool preserve_collinear) +Polygons simplify_polygons(const Polygons &subject) { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); ClipperLib::Paths output; - if (preserve_collinear) { - ClipperLib::Clipper c; - c.PreserveCollinear(true); - c.StrictlySimple(true); - c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); - c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); - } else { - output = ClipperLib::SimplifyPolygons(ClipperUtils::PolygonsProvider(subject), ClipperLib::pftNonZero); - } - + ClipperLib::Clipper c; +// c.PreserveCollinear(true); + //FIXME StrictlySimple is very expensive! Is it needed? + c.StrictlySimple(true); + c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); + c.Execute(ClipperLib::ctUnion, output, ClipperLib::pftNonZero, ClipperLib::pftNonZero); + // convert into Slic3r polygons return to_polygons(std::move(output)); } @@ -987,12 +984,10 @@ ExPolygons simplify_polygons_ex(const Polygons &subject, bool preserve_collinear { CLIPPER_UTILS_TIME_LIMIT_MILLIS(CLIPPER_UTILS_TIME_LIMIT_DEFAULT); - if (! preserve_collinear) - return union_ex(simplify_polygons(subject, false)); - ClipperLib::PolyTree polytree; ClipperLib::Clipper c; - c.PreserveCollinear(true); +// c.PreserveCollinear(true); + //FIXME StrictlySimple is very expensive! Is it needed? c.StrictlySimple(true); c.AddPaths(ClipperUtils::PolygonsProvider(subject), ClipperLib::ptSubject, true); c.Execute(ClipperLib::ctUnion, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero); diff --git a/src/libslic3r/ClipperUtils.hpp b/src/libslic3r/ClipperUtils.hpp index aaa06107d..ab967fd8f 100644 --- a/src/libslic3r/ClipperUtils.hpp +++ b/src/libslic3r/ClipperUtils.hpp @@ -596,8 +596,8 @@ void traverse_pt(const ClipperLib::PolyNodes &nodes, ExOrJustPolygons *retval) /* OTHER */ -Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject, bool preserve_collinear = false); -Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject, bool preserve_collinear = false); +Slic3r::Polygons simplify_polygons(const Slic3r::Polygons &subject); +Slic3r::ExPolygons simplify_polygons_ex(const Slic3r::Polygons &subject); Polygons top_level_islands(const Slic3r::Polygons &polygons); From fd437dcaf5f392dffb15eadcd359a4de92d0000b Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 15:48:24 +0200 Subject: [PATCH 096/115] Little refactoring of douglas_peucker() --- src/libslic3r/ExPolygon.cpp | 4 ++-- src/libslic3r/Geometry.cpp | 16 ++++++------- src/libslic3r/MultiPoint.cpp | 2 +- src/libslic3r/MultiPoint.hpp | 2 +- src/libslic3r/Polygon.cpp | 45 +++++++++++++++++++++++++----------- src/libslic3r/Polygon.hpp | 3 ++- src/libslic3r/Polyline.cpp | 2 +- 7 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/libslic3r/ExPolygon.cpp b/src/libslic3r/ExPolygon.cpp index 42f026e0b..19489bddb 100644 --- a/src/libslic3r/ExPolygon.cpp +++ b/src/libslic3r/ExPolygon.cpp @@ -184,14 +184,14 @@ Polygons ExPolygon::simplify_p(double tolerance) const { Polygon p = this->contour; p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points = MultiPoint::douglas_peucker(p.points, tolerance); p.points.pop_back(); pp.emplace_back(std::move(p)); } // holes for (Polygon p : this->holes) { p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); + p.points = MultiPoint::douglas_peucker(p.points, tolerance); p.points.pop_back(); pp.emplace_back(std::move(p)); } diff --git a/src/libslic3r/Geometry.cpp b/src/libslic3r/Geometry.cpp index 5542d73ee..f2860ea8e 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -52,15 +52,15 @@ template bool contains(const ExPolygons &vector, const Point &point); void simplify_polygons(const Polygons &polygons, double tolerance, Polygons* retval) { - Polygons pp; - for (Polygons::const_iterator it = polygons.begin(); it != polygons.end(); ++it) { - Polygon p = *it; - p.points.push_back(p.points.front()); - p.points = MultiPoint::_douglas_peucker(p.points, tolerance); - p.points.pop_back(); - pp.push_back(p); + Polygons simplified_raw; + for (const Polygon &source_polygon : polygons) { + Points simplified = MultiPoint::douglas_peucker(to_polyline(source_polygon).points, tolerance); + if (simplified.size() > 3) { + simplified.pop_back(); + simplified_raw.push_back(Polygon{ std::move(simplified) }); + } } - *retval = Slic3r::simplify_polygons(pp); + *retval = Slic3r::simplify_polygons(simplified_raw); } double linint(double value, double oldmin, double oldmax, double newmin, double newmax) diff --git a/src/libslic3r/MultiPoint.cpp b/src/libslic3r/MultiPoint.cpp index bb4d62cc0..ce54a54c0 100644 --- a/src/libslic3r/MultiPoint.cpp +++ b/src/libslic3r/MultiPoint.cpp @@ -103,7 +103,7 @@ bool MultiPoint::remove_duplicate_points() return false; } -Points MultiPoint::_douglas_peucker(const Points &pts, const double tolerance) +Points MultiPoint::douglas_peucker(const Points &pts, const double tolerance) { Points result_pts; double tolerance_sq = tolerance * tolerance; diff --git a/src/libslic3r/MultiPoint.hpp b/src/libslic3r/MultiPoint.hpp index 4cf4b5e14..62b53255b 100644 --- a/src/libslic3r/MultiPoint.hpp +++ b/src/libslic3r/MultiPoint.hpp @@ -81,7 +81,7 @@ public: } } - static Points _douglas_peucker(const Points &points, const double tolerance); + static Points douglas_peucker(const Points &points, const double tolerance); static Points visivalingam(const Points& pts, const double& tolerance); inline auto begin() { return points.begin(); } diff --git a/src/libslic3r/Polygon.cpp b/src/libslic3r/Polygon.cpp index 299e22adc..88ac1b03f 100644 --- a/src/libslic3r/Polygon.cpp +++ b/src/libslic3r/Polygon.cpp @@ -96,7 +96,7 @@ bool Polygon::make_clockwise() void Polygon::douglas_peucker(double tolerance) { this->points.push_back(this->points.front()); - Points p = MultiPoint::_douglas_peucker(this->points, tolerance); + Points p = MultiPoint::douglas_peucker(this->points, tolerance); p.pop_back(); this->points = std::move(p); } @@ -110,7 +110,7 @@ Polygons Polygon::simplify(double tolerance) const // on the whole polygon Points points = this->points; points.push_back(points.front()); - Polygon p(MultiPoint::_douglas_peucker(points, tolerance)); + Polygon p(MultiPoint::douglas_peucker(points, tolerance)); p.points.pop_back(); Polygons pp; @@ -577,23 +577,40 @@ void remove_collinear(Polygons &polys) remove_collinear(poly); } -Polygons polygons_simplify(const Polygons &source_polygons, double tolerance) +static inline void simplify_polygon_impl(const Points &points, double tolerance, bool strictly_simple, Polygons &out) +{ + Points simplified = MultiPoint::douglas_peucker(points, tolerance); + // then remove the last (repeated) point. + simplified.pop_back(); + // Simplify the decimated contour by ClipperLib. + bool ccw = ClipperLib::Area(simplified) > 0.; + for (Points& path : ClipperLib::SimplifyPolygons(ClipperUtils::SinglePathProvider(simplified), ClipperLib::pftNonZero, strictly_simple)) { + if (!ccw) + // ClipperLib likely reoriented negative area contours to become positive. Reverse holes back to CW. + std::reverse(path.begin(), path.end()); + out.emplace_back(std::move(path)); + } +} + +Polygons polygons_simplify(Polygons &&source_polygons, double tolerance, bool strictly_simple /* = true */) +{ + Polygons out; + out.reserve(source_polygons.size()); + for (Polygon &source_polygon : source_polygons) { + // Run Douglas / Peucker simplification algorithm on an open polyline (by repeating the first point at the end of the polyline), + source_polygon.points.emplace_back(source_polygon.points.front()); + simplify_polygon_impl(source_polygon.points, tolerance, strictly_simple, out); + } + return out; +} + +Polygons polygons_simplify(const Polygons &source_polygons, double tolerance, bool strictly_simple /* = true */) { Polygons out; out.reserve(source_polygons.size()); for (const Polygon &source_polygon : source_polygons) { // Run Douglas / Peucker simplification algorithm on an open polyline (by repeating the first point at the end of the polyline), - Points simplified = MultiPoint::_douglas_peucker(to_polyline(source_polygon).points, tolerance); - // then remove the last (repeated) point. - simplified.pop_back(); - // Simplify the decimated contour by ClipperLib. - bool ccw = ClipperLib::Area(simplified) > 0.; - for (Points &path : ClipperLib::SimplifyPolygons(ClipperUtils::SinglePathProvider(simplified), ClipperLib::pftNonZero)) { - if (! ccw) - // ClipperLib likely reoriented negative area contours to become positive. Reverse holes back to CW. - std::reverse(path.begin(), path.end()); - out.emplace_back(std::move(path)); - } + simplify_polygon_impl(to_polyline(source_polygon).points, tolerance, strictly_simple, out); } return out; } diff --git a/src/libslic3r/Polygon.hpp b/src/libslic3r/Polygon.hpp index bf4a087b0..e0c3958fd 100644 --- a/src/libslic3r/Polygon.hpp +++ b/src/libslic3r/Polygon.hpp @@ -149,7 +149,8 @@ inline void polygons_append(Polygons &dst, Polygons &&src) } } -Polygons polygons_simplify(const Polygons &polys, double tolerance); +Polygons polygons_simplify(Polygons &&polys, double tolerance, bool strictly_simple = true); +Polygons polygons_simplify(const Polygons &polys, double tolerance, bool strictly_simple = true); inline void polygons_rotate(Polygons &polys, double angle) { diff --git a/src/libslic3r/Polyline.cpp b/src/libslic3r/Polyline.cpp index 5743e38bd..524736575 100644 --- a/src/libslic3r/Polyline.cpp +++ b/src/libslic3r/Polyline.cpp @@ -110,7 +110,7 @@ Points Polyline::equally_spaced_points(double distance) const void Polyline::simplify(double tolerance) { - this->points = MultiPoint::_douglas_peucker(this->points, tolerance); + this->points = MultiPoint::douglas_peucker(this->points, tolerance); } #if 0 From c3178321b46008512a9f38259e49827113c4795e Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 2 May 2023 15:53:08 +0200 Subject: [PATCH 097/115] Optimization of triangle mesh slicing: scalable_allocator and hashing of shared mutexes. --- src/libslic3r/TriangleMeshSlicer.cpp | 42 ++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 460cd901e..0261b6121 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -15,6 +15,9 @@ #include #include +#include + +#include #ifndef NDEBUG // #define EXPENSIVE_DEBUG_CHECKS @@ -139,7 +142,7 @@ public: #endif }; -using IntersectionLines = std::vector; +using IntersectionLines = std::vector>; enum class FacetSliceType { NoSlice = 0, @@ -351,6 +354,21 @@ inline FacetSliceType slice_facet( return FacetSliceType::NoSlice; } +class LinesMutexes { +public: + std::mutex& operator()(size_t slice_id) { + ankerl::unordered_dense::hash hash; + return m_mutexes[hash(slice_id) % m_mutexes.size()].mutex; + } + +private: + struct CacheLineAlignedMutex + { + alignas(std::hardware_destructive_interference_size) std::mutex mutex; + }; + std::array m_mutexes; +}; + template void slice_facet_at_zs( // Scaled or unscaled vertices. transform_vertex_fn may scale zs. @@ -361,7 +379,7 @@ void slice_facet_at_zs( // Scaled or unscaled zs. If vertices have their zs scaled or transform_vertex_fn scales them, then zs have to be scaled as well. const std::vector &zs, std::vector &lines, - std::array &lines_mutex) + LinesMutexes &lines_mutex) { stl_vertex vertices[3] { transform_vertex_fn(mesh_vertices[indices(0)]), transform_vertex_fn(mesh_vertices[indices(1)]), transform_vertex_fn(mesh_vertices[indices(2)]) }; @@ -380,7 +398,7 @@ void slice_facet_at_zs( if (min_z != max_z && slice_facet(*it, vertices, indices, edge_ids, idx_vertex_lowest, false, il) == FacetSliceType::Slicing) { assert(il.edge_type != IntersectionLine::FacetEdgeType::Horizontal); size_t slice_id = it - zs.begin(); - boost::lock_guard l(lines_mutex[slice_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(slice_id)); lines[slice_id].emplace_back(il); } } @@ -395,8 +413,8 @@ static inline std::vector slice_make_lines( const std::vector &zs, const ThrowOnCancel throw_on_cancel_fn) { - std::vector lines(zs.size(), IntersectionLines()); - std::array lines_mutex; + std::vector lines(zs.size(), IntersectionLines{}); + LinesMutexes lines_mutex; tbb::parallel_for( tbb::blocked_range(0, int(indices.size())), [&vertices, &transform_vertex_fn, &indices, &face_edge_ids, &zs, &lines, &lines_mutex, throw_on_cancel_fn](const tbb::blocked_range &range) { @@ -475,7 +493,7 @@ void slice_facet_with_slabs( const int num_edges, const std::vector &zs, SlabLines &lines, - std::array &lines_mutex) + LinesMutexes &lines_mutex) { const stl_triangle_vertex_indices &indices = mesh_triangles[facet_idx]; stl_vertex vertices[3] { mesh_vertices[indices(0)], mesh_vertices[indices(1)], mesh_vertices[indices(2)] }; @@ -494,7 +512,7 @@ void slice_facet_with_slabs( auto emit_slab_edge = [&lines, &lines_mutex](IntersectionLine il, size_t slab_id, bool reverse) { if (reverse) il.reverse(); - boost::lock_guard l(lines_mutex[(slab_id + lines_mutex.size() / 2) % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(slab_id)); lines.between_slices[slab_id].emplace_back(il); }; @@ -530,7 +548,7 @@ void slice_facet_with_slabs( }; // Don't flip the FacetEdgeType::Top edge, it will be flipped when chaining. // if (! ProjectionFromTop) il.reverse(); - boost::lock_guard l(lines_mutex[line_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(line_id)); lines.at_slice[line_id].emplace_back(il); } } else { @@ -649,7 +667,7 @@ void slice_facet_with_slabs( if (! ProjectionFromTop) il.reverse(); size_t line_id = it - zs.begin(); - boost::lock_guard l(lines_mutex[line_id % lines_mutex.size()]); + boost::lock_guard l(lines_mutex(line_id)); lines.at_slice[line_id].emplace_back(il); } } @@ -804,8 +822,8 @@ inline std::pair slice_slabs_make_lines( std::pair out; SlabLines &lines_top = out.first; SlabLines &lines_bottom = out.second; - std::array lines_mutex_top; - std::array lines_mutex_bottom; + LinesMutexes lines_mutex_top; + LinesMutexes lines_mutex_bottom; if (top) { lines_top.at_slice.assign(zs.size(), IntersectionLines()); @@ -1540,7 +1558,7 @@ static std::vector make_slab_loops( } // Used to cut the mesh into two halves. -static ExPolygons make_expolygons_simple(std::vector &lines) +static ExPolygons make_expolygons_simple(IntersectionLines &lines) { ExPolygons slices; Polygons holes; From 00ea0847b81d3229135444ba9f76fd2f5cbf5e7a Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Thu, 4 May 2023 17:25:26 +0200 Subject: [PATCH 098/115] Fix of merging bridging regions: Fixed building a DAG of overlapping regions in expand_bridges_detect_orientations() --- src/libslic3r/LayerRegion.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/LayerRegion.cpp b/src/libslic3r/LayerRegion.cpp index d722f1e9c..ab52dd962 100644 --- a/src/libslic3r/LayerRegion.cpp +++ b/src/libslic3r/LayerRegion.cpp @@ -230,10 +230,13 @@ Surfaces expand_bridges_detect_orientations( bboxes[it - it_begin].overlap(bboxes[it2 - it_begin]) && // One may ignore holes, they are irrelevant for intersection test. ! intersection(it->expolygon.contour, it2->expolygon.contour).empty()) { - // The two bridge regions intersect. Give them the same group id. + // The two bridge regions intersect. Give them the same (lower) group id. uint32_t id = group_id(it->src_id); uint32_t id2 = group_id(it2->src_id); - bridges[it->src_id].group_id = bridges[it2->src_id].group_id = std::min(id, id2); + if (id < id2) + bridges[id2].group_id = id; + else + bridges[id].group_id = id2; } } } From 1bbe0c5be3bcea11b0f26a3fc2b11037e15a5fe0 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Thu, 4 May 2023 17:39:31 +0200 Subject: [PATCH 099/115] fix missing include for linux builds --- src/libslic3r/TreeModelVolumes.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/TreeModelVolumes.cpp index 10840d1b5..96f61a07d 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/TreeModelVolumes.cpp @@ -17,6 +17,7 @@ #include "Print.hpp" #include "PrintConfig.hpp" #include "Utils.hpp" +#include "format.hpp" #include From cc938e754949bed278daac5e07c2639a9f54e86e Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 5 May 2023 10:21:15 +0200 Subject: [PATCH 100/115] WIP SupportGeneratorLayerStorage refactoring. --- src/libslic3r/CMakeLists.txt | 6 +- src/libslic3r/Support/SupportLayer.hpp | 130 +++++++++++++++ src/libslic3r/Support/SupportParameters.hpp | 66 ++++++++ src/libslic3r/SupportMaterial.hpp | 165 +------------------- src/libslic3r/TreeSupport.hpp | 4 +- src/libslic3r/pchheader.hpp | 1 + 6 files changed, 205 insertions(+), 167 deletions(-) create mode 100644 src/libslic3r/Support/SupportLayer.hpp create mode 100644 src/libslic3r/Support/SupportParameters.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 53074d3bd..32d44cb5c 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -276,10 +276,12 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp - SupportSpotsGenerator.cpp - SupportSpotsGenerator.hpp + Support/SupportLayer.hpp + Support/SupportParameters.hpp SupportMaterial.cpp SupportMaterial.hpp + SupportSpotsGenerator.cpp + SupportSpotsGenerator.hpp Surface.cpp Surface.hpp SurfaceCollection.cpp diff --git a/src/libslic3r/Support/SupportLayer.hpp b/src/libslic3r/Support/SupportLayer.hpp new file mode 100644 index 000000000..913e28136 --- /dev/null +++ b/src/libslic3r/Support/SupportLayer.hpp @@ -0,0 +1,130 @@ +#ifndef slic3r_SupportLayer_hpp_ +#define slic3r_SupportLayer_hpp_ + +#include +#include + +namespace Slic3r { + +// Support layer type to be used by SupportGeneratorLayer. This type carries a much more detailed information +// about the support layer type than the final support layers stored in a PrintObject. +enum class SupporLayerType { + Unknown = 0, + // Ratft base layer, to be printed with the support material. + RaftBase, + // Raft interface layer, to be printed with the support interface material. + RaftInterface, + // Bottom contact layer placed over a top surface of an object. To be printed with a support interface material. + BottomContact, + // Dense interface layer, to be printed with the support interface material. + // This layer is separated from an object by an BottomContact layer. + BottomInterface, + // Sparse base support layer, to be printed with a support material. + Base, + // Dense interface layer, to be printed with the support interface material. + // This layer is separated from an object with TopContact layer. + TopInterface, + // Top contact layer directly supporting an overhang. To be printed with a support interface material. + TopContact, + // Some undecided type yet. It will turn into Base first, then it may turn into BottomInterface or TopInterface. + Intermediate, +}; + +// A support layer type used internally by the SupportMaterial class. This class carries a much more detailed +// information about the support layer than the layers stored in the PrintObject, mainly +// the SupportGeneratorLayer is aware of the bridging flow and the interface gaps between the object and the support. +class SupportGeneratorLayer +{ +public: + void reset() { + *this = SupportGeneratorLayer(); + } + + bool operator==(const SupportGeneratorLayer &layer2) const { + return print_z == layer2.print_z && height == layer2.height && bridging == layer2.bridging; + } + + // Order the layers by lexicographically by an increasing print_z and a decreasing layer height. + bool operator<(const SupportGeneratorLayer &layer2) const { + if (print_z < layer2.print_z) { + return true; + } else if (print_z == layer2.print_z) { + if (height > layer2.height) + return true; + else if (height == layer2.height) { + // Bridging layers first. + return bridging && ! layer2.bridging; + } else + return false; + } else + return false; + } + + void merge(SupportGeneratorLayer &&rhs) { + // The union_() does not support move semantic yet, but maybe one day it will. + this->polygons = union_(this->polygons, std::move(rhs.polygons)); + auto merge = [](std::unique_ptr &dst, std::unique_ptr &src) { + if (! dst || dst->empty()) + dst = std::move(src); + else if (src && ! src->empty()) + *dst = union_(*dst, std::move(*src)); + }; + merge(this->contact_polygons, rhs.contact_polygons); + merge(this->overhang_polygons, rhs.overhang_polygons); + merge(this->enforcer_polygons, rhs.enforcer_polygons); + rhs.reset(); + } + + // For the bridging flow, bottom_print_z will be above bottom_z to account for the vertical separation. + // For the non-bridging flow, bottom_print_z will be equal to bottom_z. + coordf_t bottom_print_z() const { return print_z - height; } + + // To sort the extremes of top / bottom interface layers. + coordf_t extreme_z() const { return (this->layer_type == SupporLayerType::TopContact) ? this->bottom_z : this->print_z; } + + SupporLayerType layer_type { SupporLayerType::Unknown }; + // Z used for printing, in unscaled coordinates. + coordf_t print_z { 0 }; + // Bottom Z of this layer. For soluble layers, bottom_z + height = print_z, + // otherwise bottom_z + gap + height = print_z. + coordf_t bottom_z { 0 }; + // Layer height in unscaled coordinates. + coordf_t height { 0 }; + // Index of a PrintObject layer_id supported by this layer. This will be set for top contact layers. + // If this is not a contact layer, it will be set to size_t(-1). + size_t idx_object_layer_above { size_t(-1) }; + // Index of a PrintObject layer_id, which supports this layer. This will be set for bottom contact layers. + // If this is not a contact layer, it will be set to size_t(-1). + size_t idx_object_layer_below { size_t(-1) }; + // Use a bridging flow when printing this support layer. + bool bridging { false }; + + // Polygons to be filled by the support pattern. + Polygons polygons; + // Currently for the contact layers only. + std::unique_ptr contact_polygons; + std::unique_ptr overhang_polygons; + // Enforcers need to be propagated independently in case the "support on build plate only" option is enabled. + std::unique_ptr enforcer_polygons; +}; + +// Layers are allocated and owned by a deque. Once a layer is allocated, it is maintained +// up to the end of a generate() method. The layer storage may be replaced by an allocator class in the future, +// which would allocate layers by multiple chunks. +#if 0 +class SupportGeneratorLayerStorage { +public: +private: + template + using Allocator = tbb::scalable_allocator; + Slic3r::deque> m_data; + tbb::spin_mutex m_mutex; +}; +#else +#endif +using SupportGeneratorLayerStorage = std::deque; +using SupportGeneratorLayersPtr = std::vector; + +} // namespace Slic3r + +#endif /* slic3r_SupportLayer_hpp_ */ diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp new file mode 100644 index 000000000..fd4f1f8b7 --- /dev/null +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -0,0 +1,66 @@ +#ifndef slic3r_SupportParameters_hpp_ +#define slic3r_SupportParameters_hpp_ + +#include "../libslic3r.h" +#include "../Flow.hpp" + +namespace Slic3r { + +class PrintObject; +enum InfillPattern : int; + +struct SupportParameters { + SupportParameters(const PrintObject &object); + + // Flow at the 1st print layer. + Flow first_layer_flow; + // Flow at the support base (neither top, nor bottom interface). + // Also flow at the raft base with the exception of raft interface and contact layers. + Flow support_material_flow; + // Flow at the top interface and contact layers. + Flow support_material_interface_flow; + // Flow at the bottom interfaces and contacts. + Flow support_material_bottom_interface_flow; + // Flow at raft inteface & contact layers. + Flow raft_interface_flow; + // Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder? + bool can_merge_support_regions; + + coordf_t support_layer_height_min; +// coordf_t support_layer_height_max; + + coordf_t gap_xy; + + float base_angle; + float interface_angle; + + // Density of the top / bottom interface and contact layers. + coordf_t interface_density; + // Density of the raft interface and contact layers. + coordf_t raft_interface_density; + // Density of the base support layers. + coordf_t support_density; + + // Pattern of the sparse infill including sparse raft layers. + InfillPattern base_fill_pattern; + // Pattern of the top / bottom interface and contact layers. + InfillPattern interface_fill_pattern; + // Pattern of the raft interface and contact layers. + InfillPattern raft_interface_fill_pattern; + // Pattern of the contact layers. + InfillPattern contact_fill_pattern; + // Shall the sparse (base) layers be printed with a single perimeter line (sheath) for robustness? + bool with_sheath; + + float raft_angle_1st_layer; + float raft_angle_base; + float raft_angle_interface; + + // Produce a raft interface angle for a given SupportLayer::interface_id() + float raft_interface_angle(size_t interface_id) const + { return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); } +}; + +} // namespace Slic3r + +#endif /* slic3r_SupportParameters_hpp_ */ diff --git a/src/libslic3r/SupportMaterial.hpp b/src/libslic3r/SupportMaterial.hpp index 2bd321144..24dc8507e 100644 --- a/src/libslic3r/SupportMaterial.hpp +++ b/src/libslic3r/SupportMaterial.hpp @@ -5,171 +5,12 @@ #include "PrintConfig.hpp" #include "Slicing.hpp" +#include "Support/SupportLayer.hpp" +#include "Support/SupportParameters.hpp" + namespace Slic3r { class PrintObject; -class PrintConfig; -class PrintObjectConfig; - -// Support layer type to be used by SupportGeneratorLayer. This type carries a much more detailed information -// about the support layer type than the final support layers stored in a PrintObject. -enum class SupporLayerType { - Unknown = 0, - // Ratft base layer, to be printed with the support material. - RaftBase, - // Raft interface layer, to be printed with the support interface material. - RaftInterface, - // Bottom contact layer placed over a top surface of an object. To be printed with a support interface material. - BottomContact, - // Dense interface layer, to be printed with the support interface material. - // This layer is separated from an object by an BottomContact layer. - BottomInterface, - // Sparse base support layer, to be printed with a support material. - Base, - // Dense interface layer, to be printed with the support interface material. - // This layer is separated from an object with TopContact layer. - TopInterface, - // Top contact layer directly supporting an overhang. To be printed with a support interface material. - TopContact, - // Some undecided type yet. It will turn into Base first, then it may turn into BottomInterface or TopInterface. - Intermediate, -}; - -// A support layer type used internally by the SupportMaterial class. This class carries a much more detailed -// information about the support layer than the layers stored in the PrintObject, mainly -// the SupportGeneratorLayer is aware of the bridging flow and the interface gaps between the object and the support. -class SupportGeneratorLayer -{ -public: - void reset() { - *this = SupportGeneratorLayer(); - } - - bool operator==(const SupportGeneratorLayer &layer2) const { - return print_z == layer2.print_z && height == layer2.height && bridging == layer2.bridging; - } - - // Order the layers by lexicographically by an increasing print_z and a decreasing layer height. - bool operator<(const SupportGeneratorLayer &layer2) const { - if (print_z < layer2.print_z) { - return true; - } else if (print_z == layer2.print_z) { - if (height > layer2.height) - return true; - else if (height == layer2.height) { - // Bridging layers first. - return bridging && ! layer2.bridging; - } else - return false; - } else - return false; - } - - void merge(SupportGeneratorLayer &&rhs) { - // The union_() does not support move semantic yet, but maybe one day it will. - this->polygons = union_(this->polygons, std::move(rhs.polygons)); - auto merge = [](std::unique_ptr &dst, std::unique_ptr &src) { - if (! dst || dst->empty()) - dst = std::move(src); - else if (src && ! src->empty()) - *dst = union_(*dst, std::move(*src)); - }; - merge(this->contact_polygons, rhs.contact_polygons); - merge(this->overhang_polygons, rhs.overhang_polygons); - merge(this->enforcer_polygons, rhs.enforcer_polygons); - rhs.reset(); - } - - // For the bridging flow, bottom_print_z will be above bottom_z to account for the vertical separation. - // For the non-bridging flow, bottom_print_z will be equal to bottom_z. - coordf_t bottom_print_z() const { return print_z - height; } - - // To sort the extremes of top / bottom interface layers. - coordf_t extreme_z() const { return (this->layer_type == SupporLayerType::TopContact) ? this->bottom_z : this->print_z; } - - SupporLayerType layer_type { SupporLayerType::Unknown }; - // Z used for printing, in unscaled coordinates. - coordf_t print_z { 0 }; - // Bottom Z of this layer. For soluble layers, bottom_z + height = print_z, - // otherwise bottom_z + gap + height = print_z. - coordf_t bottom_z { 0 }; - // Layer height in unscaled coordinates. - coordf_t height { 0 }; - // Index of a PrintObject layer_id supported by this layer. This will be set for top contact layers. - // If this is not a contact layer, it will be set to size_t(-1). - size_t idx_object_layer_above { size_t(-1) }; - // Index of a PrintObject layer_id, which supports this layer. This will be set for bottom contact layers. - // If this is not a contact layer, it will be set to size_t(-1). - size_t idx_object_layer_below { size_t(-1) }; - // Use a bridging flow when printing this support layer. - bool bridging { false }; - - // Polygons to be filled by the support pattern. - Polygons polygons; - // Currently for the contact layers only. - std::unique_ptr contact_polygons; - std::unique_ptr overhang_polygons; - // Enforcers need to be propagated independently in case the "support on build plate only" option is enabled. - std::unique_ptr enforcer_polygons; -}; - -// Layers are allocated and owned by a deque. Once a layer is allocated, it is maintained -// up to the end of a generate() method. The layer storage may be replaced by an allocator class in the future, -// which would allocate layers by multiple chunks. -using SupportGeneratorLayerStorage = std::deque; -using SupportGeneratorLayersPtr = std::vector; - -struct SupportParameters { - SupportParameters(const PrintObject &object); - - // Flow at the 1st print layer. - Flow first_layer_flow; - // Flow at the support base (neither top, nor bottom interface). - // Also flow at the raft base with the exception of raft interface and contact layers. - Flow support_material_flow; - // Flow at the top interface and contact layers. - Flow support_material_interface_flow; - // Flow at the bottom interfaces and contacts. - Flow support_material_bottom_interface_flow; - // Flow at raft inteface & contact layers. - Flow raft_interface_flow; - // Is merging of regions allowed? Could the interface & base support regions be printed with the same extruder? - bool can_merge_support_regions; - - coordf_t support_layer_height_min; -// coordf_t support_layer_height_max; - - coordf_t gap_xy; - - float base_angle; - float interface_angle; - - // Density of the top / bottom interface and contact layers. - coordf_t interface_density; - // Density of the raft interface and contact layers. - coordf_t raft_interface_density; - // Density of the base support layers. - coordf_t support_density; - - // Pattern of the sparse infill including sparse raft layers. - InfillPattern base_fill_pattern; - // Pattern of the top / bottom interface and contact layers. - InfillPattern interface_fill_pattern; - // Pattern of the raft interface and contact layers. - InfillPattern raft_interface_fill_pattern; - // Pattern of the contact layers. - InfillPattern contact_fill_pattern; - // Shall the sparse (base) layers be printed with a single perimeter line (sheath) for robustness? - bool with_sheath; - - float raft_angle_1st_layer; - float raft_angle_base; - float raft_angle_interface; - - // Produce a raft interface angle for a given SupportLayer::interface_id() - float raft_interface_angle(size_t interface_id) const - { return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); } -}; // Remove bridges from support contact areas. // To be called if PrintObjectConfig::dont_support_bridges. diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/TreeSupport.hpp index 2c87a132d..2ed5d50ec 100644 --- a/src/libslic3r/TreeSupport.hpp +++ b/src/libslic3r/TreeSupport.hpp @@ -11,6 +11,7 @@ #include "TreeModelVolumes.hpp" #include "Point.hpp" +#include "Support/SupportLayer.hpp" #include @@ -39,10 +40,7 @@ namespace Slic3r // Forward declarations class Print; class PrintObject; -class SupportGeneratorLayer; struct SlicingParameters; -using SupportGeneratorLayerStorage = std::deque; -using SupportGeneratorLayersPtr = std::vector; namespace FFFTreeSupport { diff --git a/src/libslic3r/pchheader.hpp b/src/libslic3r/pchheader.hpp index 9017a5dea..e71b1461c 100644 --- a/src/libslic3r/pchheader.hpp +++ b/src/libslic3r/pchheader.hpp @@ -107,6 +107,7 @@ #include #include +#include #include #include From a236351fd3043e6a8a007830aaabc423387b1c6c Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 5 May 2023 12:59:01 +0200 Subject: [PATCH 101/115] Supports refactoring: Split FFF supports into multiple files, enclosed into namespaces. --- src/libslic3r/CMakeLists.txt | 5 + src/libslic3r/Support/SupportCommon.cpp | 1807 +++++++++++++++++ src/libslic3r/Support/SupportCommon.hpp | 138 ++ src/libslic3r/Support/SupportDebug.cpp | 108 + src/libslic3r/Support/SupportDebug.hpp | 18 + src/libslic3r/Support/SupportLayer.hpp | 26 +- src/libslic3r/Support/SupportParameters.cpp | 116 ++ src/libslic3r/Support/SupportParameters.hpp | 4 + src/libslic3r/SupportMaterial.cpp | 2013 +------------------ src/libslic3r/SupportMaterial.hpp | 52 +- src/libslic3r/TreeSupport.cpp | 44 +- 11 files changed, 2259 insertions(+), 2072 deletions(-) create mode 100644 src/libslic3r/Support/SupportCommon.cpp create mode 100644 src/libslic3r/Support/SupportCommon.hpp create mode 100644 src/libslic3r/Support/SupportDebug.cpp create mode 100644 src/libslic3r/Support/SupportDebug.hpp create mode 100644 src/libslic3r/Support/SupportParameters.cpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 32d44cb5c..11f7adf76 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -276,7 +276,12 @@ set(SLIC3R_SOURCES SlicingAdaptive.hpp Subdivide.cpp Subdivide.hpp + Support/SupportCommon.cpp + Support/SupportCommon.hpp + Support/SupportDebug.cpp + Support/SupportDebug.hpp Support/SupportLayer.hpp + Support/SupportParameters.cpp Support/SupportParameters.hpp SupportMaterial.cpp SupportMaterial.hpp diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp new file mode 100644 index 000000000..8825d8dd2 --- /dev/null +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -0,0 +1,1807 @@ +#include "../ClipperUtils.hpp" +#include "../ExtrusionEntityCollection.hpp" +#include "../Layer.hpp" +#include "../Print.hpp" +#include "../Fill/FillBase.hpp" +#include "../Geometry.hpp" +#include "../Point.hpp" + +#include +#include + +#include + +#include "SupportCommon.hpp" +#include "SupportLayer.hpp" +#include "SupportParameters.hpp" + +// #define SLIC3R_DEBUG + +// Make assert active if SLIC3R_DEBUG +#ifdef SLIC3R_DEBUG + #define DEBUG + #define _DEBUG + #undef NDEBUG + #include "utils.hpp" + #include "SVG.hpp" +#endif + +#include + +namespace Slic3r::FFFSupport { + +// how much we extend support around the actual contact area +//FIXME this should be dependent on the nozzle diameter! +#define SUPPORT_MATERIAL_MARGIN 1.5 + +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 3. +//#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5 +#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. + +void remove_bridges_from_contacts( + const PrintConfig &print_config, + const Layer &lower_layer, + const LayerRegion &layerm, + float fw, + Polygons &contact_polygons) +{ + // compute the area of bridging perimeters + Polygons bridges; + { + // Surface supporting this layer, expanded by 0.5 * nozzle_diameter, as we consider this kind of overhang to be sufficiently supported. + Polygons lower_grown_slices = expand(lower_layer.lslices, + //FIXME to mimic the decision in the perimeter generator, we should use half the external perimeter width. + 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().perimeter_extruder-1))), + SUPPORT_SURFACES_OFFSET_PARAMETERS); + // Collect perimeters of this layer. + //FIXME split_at_first_point() could split a bridge mid-way + #if 0 + Polylines overhang_perimeters = layerm.perimeters.as_polylines(); + // workaround for Clipper bug, see Slic3r::Polygon::clip_as_polyline() + for (Polyline &polyline : overhang_perimeters) + polyline.points[0].x += 1; + // Trim the perimeters of this layer by the lower layer to get the unsupported pieces of perimeters. + overhang_perimeters = diff_pl(overhang_perimeters, lower_grown_slices); + #else + Polylines overhang_perimeters = diff_pl(layerm.perimeters().as_polylines(), lower_grown_slices); + #endif + + // only consider straight overhangs + // only consider overhangs having endpoints inside layer's slices + // convert bridging polylines into polygons by inflating them with their thickness + // since we're dealing with bridges, we can't assume width is larger than spacing, + // so we take the largest value and also apply safety offset to be ensure no gaps + // are left in between + Flow perimeter_bridge_flow = layerm.bridging_flow(frPerimeter); + //FIXME one may want to use a maximum of bridging flow width and normal flow width, as the perimeters are calculated using the normal flow + // and then turned to bridging flow, thus their centerlines are derived from non-bridging flow and expanding them by a bridging flow + // may not expand them to the edge of their respective islands. + const float w = float(0.5 * std::max(perimeter_bridge_flow.scaled_width(), perimeter_bridge_flow.scaled_spacing())) + scaled(0.001); + for (Polyline &polyline : overhang_perimeters) + if (polyline.is_straight()) { + // This is a bridge + polyline.extend_start(fw); + polyline.extend_end(fw); + // Is the straight perimeter segment supported at both sides? + Point pts[2] = { polyline.first_point(), polyline.last_point() }; + bool supported[2] = { false, false }; + for (size_t i = 0; i < lower_layer.lslices.size() && ! (supported[0] && supported[1]); ++ i) + for (int j = 0; j < 2; ++ j) + if (! supported[j] && lower_layer.lslices_ex[i].bbox.contains(pts[j]) && lower_layer.lslices[i].contains(pts[j])) + supported[j] = true; + if (supported[0] && supported[1]) + // Offset a polyline into a thick line. + polygons_append(bridges, offset(polyline, w)); + } + bridges = union_(bridges); + } + // remove the entire bridges and only support the unsupported edges + //FIXME the brided regions are already collected as layerm.bridged. Use it? + for (const Surface &surface : layerm.fill_surfaces()) + if (surface.surface_type == stBottomBridge && surface.bridge_angle >= 0.0) + polygons_append(bridges, surface.expolygon); + //FIXME add the gap filled areas. Extrude the gaps with a bridge flow? + // Remove the unsupported ends of the bridges from the bridged areas. + //FIXME add supports at regular intervals to support long bridges! + bridges = diff(bridges, + // Offset unsupported edges into polygons. + offset(layerm.unsupported_bridge_edges(), scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS)); + // Remove bridged areas from the supported areas. + contact_polygons = diff(contact_polygons, bridges, ApplySafetyOffset::Yes); + + #ifdef SLIC3R_DEBUG + static int iRun = 0; + SVG::export_expolygons(debug_out_path("support-top-contacts-remove-bridges-run%d.svg", iRun ++), + { { { union_ex(offset(layerm.unsupported_bridge_edges(), scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS)) }, { "unsupported_bridge_edges", "orange", 0.5f } }, + { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, + { { union_ex(bridges) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); + #endif /* SLIC3R_DEBUG */ +} + +SupportGeneratorLayersPtr generate_raft_base( + const PrintObject &object, + const SupportParameters &support_params, + const SlicingParameters &slicing_params, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers, + const SupportGeneratorLayersPtr &base_layers, + SupportGeneratorLayerStorage &layer_storage) +{ + // If there is brim to be generated, calculate the trimming regions. + Polygons brim; + if (object.has_brim()) { + // The object does not have a raft. + // Calculate the area covered by the brim. + const BrimType brim_type = object.config().brim_type; + const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; + const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; + const auto brim_separation = scaled(object.config().brim_separation.value + object.config().brim_width.value); + for (const ExPolygon &ex : object.layers().front()->lslices) { + if (brim_outer && brim_inner) + polygons_append(brim, offset(ex, brim_separation)); + else { + if (brim_outer) + polygons_append(brim, offset(ex.contour, brim_separation, ClipperLib::jtRound, float(scale_(0.1)))); + else + brim.emplace_back(ex.contour); + if (brim_inner) { + Polygons holes = ex.holes; + polygons_reverse(holes); + holes = shrink(holes, brim_separation, ClipperLib::jtRound, float(scale_(0.1))); + polygons_reverse(holes); + polygons_append(brim, std::move(holes)); + } else + polygons_append(brim, ex.holes); + } + } + brim = union_(brim); + } + + // How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed. + const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON)); + const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); + SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front(); + SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front(); + SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front(); + SupportGeneratorLayer *columns_base = base_layers .empty() ? nullptr : base_layers .front(); + if (contacts != nullptr && contacts->print_z > std::max(slicing_params.first_print_layer_height, slicing_params.raft_contact_top_z) + EPSILON) + // This is not the raft contact layer. + contacts = nullptr; + if (interfaces != nullptr && interfaces->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) + // This is not the raft column base layer. + interfaces = nullptr; + if (base_interfaces != nullptr && base_interfaces->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) + // This is not the raft column base layer. + base_interfaces = nullptr; + if (columns_base != nullptr && columns_base->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) + // This is not the raft interface layer. + columns_base = nullptr; + + Polygons interface_polygons; + if (contacts != nullptr && ! contacts->polygons.empty()) + polygons_append(interface_polygons, expand(contacts->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + if (interfaces != nullptr && ! interfaces->polygons.empty()) + polygons_append(interface_polygons, expand(interfaces->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + if (base_interfaces != nullptr && ! base_interfaces->polygons.empty()) + polygons_append(interface_polygons, expand(base_interfaces->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); + + // Output vector. + SupportGeneratorLayersPtr raft_layers; + + if (slicing_params.raft_layers() > 1) { + Polygons base; + Polygons columns; + Polygons first_layer; + if (columns_base != nullptr) { + if (columns_base->bottom_print_z() > slicing_params.raft_interface_top_z - EPSILON) { + // Classic supports with colums above the raft interface. + base = columns_base->polygons; + columns = base; + if (! interface_polygons.empty()) + // Trim the 1st layer columns with the inflated interface polygons. + columns = diff(columns, interface_polygons); + } else { + // Organic supports with raft on print bed. + assert(is_approx(columns_base->print_z, slicing_params.first_print_layer_height)); + first_layer = columns_base->polygons; + } + } + if (! interface_polygons.empty()) { + // Merge the untrimmed columns base with the expanded raft interface, to be used for the support base and interface. + base = union_(base, interface_polygons); + } + // Do not add the raft contact layer, only add the raft layers below the contact layer. + // Insert the 1st layer. + { + SupportGeneratorLayer &new_layer = layer_storage.allocate_unguarded(slicing_params.base_raft_layers > 0 ? SupporLayerType::RaftBase : SupporLayerType::RaftInterface); + raft_layers.push_back(&new_layer); + new_layer.print_z = slicing_params.first_print_layer_height; + new_layer.height = slicing_params.first_print_layer_height; + new_layer.bottom_z = 0.; + first_layer = union_(std::move(first_layer), base); + new_layer.polygons = inflate_factor_1st_layer > 0 ? expand(first_layer, inflate_factor_1st_layer) : first_layer; + } + // Insert the base layers. + for (size_t i = 1; i < slicing_params.base_raft_layers; ++ i) { + coordf_t print_z = raft_layers.back()->print_z; + SupportGeneratorLayer &new_layer = layer_storage.allocate_unguarded(SupporLayerType::RaftBase); + raft_layers.push_back(&new_layer); + new_layer.print_z = print_z + slicing_params.base_raft_layer_height; + new_layer.height = slicing_params.base_raft_layer_height; + new_layer.bottom_z = print_z; + new_layer.polygons = base; + } + // Insert the interface layers. + for (size_t i = 1; i < slicing_params.interface_raft_layers; ++ i) { + coordf_t print_z = raft_layers.back()->print_z; + SupportGeneratorLayer &new_layer = layer_storage.allocate_unguarded(SupporLayerType::RaftInterface); + raft_layers.push_back(&new_layer); + new_layer.print_z = print_z + slicing_params.interface_raft_layer_height; + new_layer.height = slicing_params.interface_raft_layer_height; + new_layer.bottom_z = print_z; + new_layer.polygons = interface_polygons; + //FIXME misusing contact_polygons for support columns. + new_layer.contact_polygons = std::make_unique(columns); + } + } else { + if (columns_base != nullptr) { + // Expand the bases of the support columns in the 1st layer. + Polygons &raft = columns_base->polygons; + Polygons trimming = offset(object.layers().front()->lslices, (float)scale_(support_params.gap_xy), SUPPORT_SURFACES_OFFSET_PARAMETERS); + if (inflate_factor_1st_layer > SCALED_EPSILON) { + // Inflate in multiple steps to avoid leaking of the support 1st layer through object walls. + auto nsteps = std::max(5, int(ceil(inflate_factor_1st_layer / support_params.first_layer_flow.scaled_width()))); + float step = inflate_factor_1st_layer / nsteps; + for (int i = 0; i < nsteps; ++ i) + raft = diff(expand(raft, step), trimming); + } else + raft = diff(raft, trimming); + if (! interface_polygons.empty()) + columns_base->polygons = diff(columns_base->polygons, interface_polygons); + } + if (! brim.empty()) { + if (columns_base) + columns_base->polygons = diff(columns_base->polygons, brim); + if (contacts) + contacts->polygons = diff(contacts->polygons, brim); + if (interfaces) + interfaces->polygons = diff(interfaces->polygons, brim); + if (base_interfaces) + base_interfaces->polygons = diff(base_interfaces->polygons, brim); + } + } + + return raft_layers; +} + +static inline void fill_expolygon_generate_paths( + ExtrusionEntitiesPtr &dst, + ExPolygon &&expolygon, + Fill *filler, + const FillParams &fill_params, + float density, + ExtrusionRole role, + const Flow &flow) +{ + Surface surface(stInternal, std::move(expolygon)); + Polylines polylines; + try { + assert(!fill_params.use_arachne); + polylines = filler->fill_surface(&surface, fill_params); + } catch (InfillFailedException &) { + } + extrusion_entities_append_paths( + dst, + std::move(polylines), + role, + flow.mm3_per_mm(), flow.width(), flow.height()); +} + +static inline void fill_expolygons_generate_paths( + ExtrusionEntitiesPtr &dst, + ExPolygons &&expolygons, + Fill *filler, + const FillParams &fill_params, + float density, + ExtrusionRole role, + const Flow &flow) +{ + for (ExPolygon &expoly : expolygons) + fill_expolygon_generate_paths(dst, std::move(expoly), filler, fill_params, density, role, flow); +} + +static inline void fill_expolygons_generate_paths( + ExtrusionEntitiesPtr &dst, + ExPolygons &&expolygons, + Fill *filler, + float density, + ExtrusionRole role, + const Flow &flow) +{ + FillParams fill_params; + fill_params.density = density; + fill_params.dont_adjust = true; + fill_expolygons_generate_paths(dst, std::move(expolygons), filler, fill_params, density, role, flow); +} + +static Polylines draw_perimeters(const ExPolygon &expoly, double clip_length) +{ + // Draw the perimeters. + Polylines polylines; + polylines.reserve(expoly.holes.size() + 1); + for (size_t i = 0; i <= expoly.holes.size(); ++ i) { + Polyline pl(i == 0 ? expoly.contour.points : expoly.holes[i - 1].points); + pl.points.emplace_back(pl.points.front()); + if (i > 0) + // It is a hole, reverse it. + pl.reverse(); + // so that all contours are CCW oriented. + pl.clip_end(clip_length); + polylines.emplace_back(std::move(pl)); + } + return polylines; +} + +static inline void tree_supports_generate_paths( + ExtrusionEntitiesPtr &dst, + const Polygons &polygons, + const Flow &flow) +{ + // Offset expolygon inside, returns number of expolygons collected (0 or 1). + // Vertices of output paths are marked with Z = source contour index of the expoly. + // Vertices at the intersection of source contours are marked with Z = -1. + auto shrink_expolygon_with_contour_idx = [](const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib_Z::Paths &out) -> int + { + assert(delta > 0); + auto append_paths_with_z = [](ClipperLib::Paths &src, coord_t contour_idx, ClipperLib_Z::Paths &dst) { + dst.reserve(next_highest_power_of_2(dst.size() + src.size())); + for (const ClipperLib::Path &contour : src) { + ClipperLib_Z::Path tmp; + tmp.reserve(contour.size()); + for (const Point &p : contour) + tmp.emplace_back(p.x(), p.y(), contour_idx); + dst.emplace_back(std::move(tmp)); + } + }; + + // 1) Offset the outer contour. + ClipperLib_Z::Paths contours; + { + ClipperLib::ClipperOffset co; + if (joinType == jtRound) + co.ArcTolerance = miterLimit; + else + co.MiterLimit = miterLimit; + co.ShortestEdgeLength = double(delta * 0.005); + co.AddPath(expoly.contour.points, joinType, ClipperLib::etClosedPolygon); + ClipperLib::Paths contours_raw; + co.Execute(contours_raw, - delta); + if (contours_raw.empty()) + // No need to try to offset the holes. + return 0; + append_paths_with_z(contours_raw, 0, contours); + } + + if (expoly.holes.empty()) { + // No need to subtract holes from the offsetted expolygon, we are done. + append(out, std::move(contours)); + } else { + // 2) Offset the holes one by one, collect the offsetted holes. + ClipperLib_Z::Paths holes; + { + for (const Polygon &hole : expoly.holes) { + ClipperLib::ClipperOffset co; + if (joinType == jtRound) + co.ArcTolerance = miterLimit; + else + co.MiterLimit = miterLimit; + co.ShortestEdgeLength = double(delta * 0.005); + co.AddPath(hole.points, joinType, ClipperLib::etClosedPolygon); + ClipperLib::Paths out2; + // Execute reorients the contours so that the outer most contour has a positive area. Thus the output + // contours will be CCW oriented even though the input paths are CW oriented. + // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. + co.Execute(out2, delta); + append_paths_with_z(out2, 1 + (&hole - expoly.holes.data()), holes); + } + } + + // 3) Subtract holes from the contours. + if (holes.empty()) { + // No hole remaining after an offset. Just copy the outer contour. + append(out, std::move(contours)); + } else { + // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. + // Subtract the offsetted holes from the offsetted contours. + ClipperLib_Z::Clipper clipper; + clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot, const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) { + //pt.z() = std::max(std::max(e1bot.z(), e1top.z()), std::max(e2bot.z(), e2top.z())); + // Just mark the intersection. + pt.z() = -1; + }); + clipper.AddPaths(contours, ClipperLib_Z::ptSubject, true); + clipper.AddPaths(holes, ClipperLib_Z::ptClip, true); + ClipperLib_Z::Paths output; + clipper.Execute(ClipperLib_Z::ctDifference, output, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); + if (! output.empty()) { + append(out, std::move(output)); + } else { + // The offsetted holes have eaten up the offsetted outer contour. + return 0; + } + } + } + + return 1; + }; + + const double spacing = flow.scaled_spacing(); + // Clip the sheath path to avoid the extruder to get exactly on the first point of the loop. + const double clip_length = spacing * 0.15; + const double anchor_length = spacing * 6.; + ClipperLib_Z::Paths anchor_candidates; + for (ExPolygon& expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5 * flow.scaled_width()))) { + std::unique_ptr eec; + double area = expoly.area(); + if (area > sqr(scaled(5.))) { + eec = std::make_unique(); + // Don't reoder internal / external loops of the same island, always start with the internal loop. + eec->no_sort = true; + // Make the tree branch stable by adding another perimeter. + ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width()); + if (level2.size() == 1) { + Polylines polylines; + extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), + // Disable reversal of the path, always start with the anchor, always print CCW. + false); + expoly = level2.front(); + } + } + + // Try to produce one more perimeter to place the seam anchor. + // First genrate a 2nd perimeter loop as a source for anchor candidates. + // The anchor candidate points are annotated with an index of the source contour or with -1 if on intersection. + anchor_candidates.clear(); + shrink_expolygon_with_contour_idx(expoly, flow.scaled_width(), DefaultJoinType, 1.2, anchor_candidates); + // Orient all contours CW. + for (auto &path : anchor_candidates) + if (ClipperLib_Z::Area(path) > 0) + std::reverse(path.begin(), path.end()); + + // Draw the perimeters. + Polylines polylines; + polylines.reserve(expoly.holes.size() + 1); + for (size_t idx_loop = 0; idx_loop < expoly.num_contours(); ++ idx_loop) { + // Open the loop with a seam. + const Polygon &loop = expoly.contour_or_hole(idx_loop); + Polyline pl(loop.points); + // Orient all contours CW, because the anchor will be added to the end of polyline while we want to start a loop with the anchor. + if (idx_loop == 0) + // It is an outer contour. + pl.reverse(); + pl.points.emplace_back(pl.points.front()); + pl.clip_end(clip_length); + if (pl.size() < 2) + continue; + // Find the foot of the seam point on anchor_candidates. Only pick an anchor point that was created by offsetting the source contour. + ClipperLib_Z::Path *closest_contour = nullptr; + Vec2d closest_point; + int closest_point_idx = -1; + double closest_point_t; + double d2min = std::numeric_limits::max(); + Vec2d seam_pt = pl.back().cast(); + for (ClipperLib_Z::Path &path : anchor_candidates) + for (int i = 0; i < path.size(); ++ i) { + int j = next_idx_modulo(i, path); + if (path[i].z() == idx_loop || path[j].z() == idx_loop) { + Vec2d pi(path[i].x(), path[i].y()); + Vec2d pj(path[j].x(), path[j].y()); + Vec2d v = pj - pi; + Vec2d w = seam_pt - pi; + auto l2 = v.squaredNorm(); + auto t = std::clamp((l2 == 0) ? 0 : v.dot(w) / l2, 0., 1.); + if ((path[i].z() == idx_loop || t > EPSILON) && (path[j].z() == idx_loop || t < 1. - EPSILON)) { + // Closest point. + Vec2d fp = pi + v * t; + double d2 = (fp - seam_pt).squaredNorm(); + if (d2 < d2min) { + d2min = d2; + closest_contour = &path; + closest_point = fp; + closest_point_idx = i; + closest_point_t = t; + } + } + } + } + if (d2min < sqr(flow.scaled_width() * 3.)) { + // Try to cut an anchor from the closest_contour. + // Both closest_contour and pl are CW oriented. + pl.points.emplace_back(closest_point.cast()); + const ClipperLib_Z::Path &path = *closest_contour; + double remaining_length = anchor_length - (seam_pt - closest_point).norm(); + int i = closest_point_idx; + int j = next_idx_modulo(i, *closest_contour); + Vec2d pi(path[i].x(), path[i].y()); + Vec2d pj(path[j].x(), path[j].y()); + Vec2d v = pj - pi; + double l = v.norm(); + if (remaining_length < (1. - closest_point_t) * l) { + // Just trim the current line. + pl.points.emplace_back((closest_point + v * (remaining_length / l)).cast()); + } else { + // Take the rest of the current line, continue with the other lines. + pl.points.emplace_back(path[j].x(), path[j].y()); + pi = pj; + for (i = j; path[i].z() == idx_loop && remaining_length > 0; i = j, pi = pj) { + j = next_idx_modulo(i, path); + pj = Vec2d(path[j].x(), path[j].y()); + v = pj - pi; + l = v.norm(); + if (i == closest_point_idx) { + // Back at the first segment. Most likely this should not happen and we may end the anchor. + break; + } + if (remaining_length <= l) { + pl.points.emplace_back((pi + v * (remaining_length / l)).cast()); + break; + } + pl.points.emplace_back(path[j].x(), path[j].y()); + remaining_length -= l; + } + } + } + // Start with the anchor. + pl.reverse(); + polylines.emplace_back(std::move(pl)); + } + + ExtrusionEntitiesPtr &out = eec ? eec->entities : dst; + extrusion_entities_append_paths(out, std::move(polylines), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), + // Disable reversal of the path, always start with the anchor, always print CCW. + false); + if (eec) { + std::reverse(eec->entities.begin(), eec->entities.end()); + dst.emplace_back(eec.release()); + } + } +} + +static inline void fill_expolygons_with_sheath_generate_paths( + ExtrusionEntitiesPtr &dst, + const Polygons &polygons, + Fill *filler, + float density, + ExtrusionRole role, + const Flow &flow, + bool with_sheath, + bool no_sort) +{ + if (polygons.empty()) + return; + + if (! with_sheath) { + fill_expolygons_generate_paths(dst, closing_ex(polygons, float(SCALED_EPSILON)), filler, density, role, flow); + return; + } + + FillParams fill_params; + fill_params.density = density; + fill_params.dont_adjust = true; + + const double spacing = flow.scaled_spacing(); + // Clip the sheath path to avoid the extruder to get exactly on the first point of the loop. + const double clip_length = spacing * 0.15; + + for (ExPolygon &expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5*flow.scaled_width()))) { + // Don't reorder the skirt and its infills. + std::unique_ptr eec; + if (no_sort) { + eec = std::make_unique(); + eec->no_sort = true; + } + ExtrusionEntitiesPtr &out = no_sort ? eec->entities : dst; + extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height()); + // Fill in the rest. + fill_expolygons_generate_paths(out, offset_ex(expoly, float(-0.4 * spacing)), filler, fill_params, density, role, flow); + if (no_sort && ! eec->empty()) + dst.emplace_back(eec.release()); + } +} + +// Support layers, partially processed. +struct SupportGeneratorLayerExtruded +{ + SupportGeneratorLayerExtruded& operator=(SupportGeneratorLayerExtruded &&rhs) { + this->layer = rhs.layer; + this->extrusions = std::move(rhs.extrusions); + m_polygons_to_extrude = std::move(rhs.m_polygons_to_extrude); + rhs.layer = nullptr; + return *this; + } + + bool empty() const { + return layer == nullptr || layer->polygons.empty(); + } + + void set_polygons_to_extrude(Polygons &&polygons) { + if (m_polygons_to_extrude == nullptr) + m_polygons_to_extrude = std::make_unique(std::move(polygons)); + else + *m_polygons_to_extrude = std::move(polygons); + } + Polygons& polygons_to_extrude() { return (m_polygons_to_extrude == nullptr) ? layer->polygons : *m_polygons_to_extrude; } + const Polygons& polygons_to_extrude() const { return (m_polygons_to_extrude == nullptr) ? layer->polygons : *m_polygons_to_extrude; } + + bool could_merge(const SupportGeneratorLayerExtruded &other) const { + return ! this->empty() && ! other.empty() && + std::abs(this->layer->height - other.layer->height) < EPSILON && + this->layer->bridging == other.layer->bridging; + } + + // Merge regions, perform boolean union over the merged polygons. + void merge(SupportGeneratorLayerExtruded &&other) { + assert(this->could_merge(other)); + // 1) Merge the rest polygons to extrude, if there are any. + if (other.m_polygons_to_extrude != nullptr) { + if (m_polygons_to_extrude == nullptr) { + // This layer has no extrusions generated yet, if it has no m_polygons_to_extrude (its area to extrude was not reduced yet). + assert(this->extrusions.empty()); + m_polygons_to_extrude = std::make_unique(this->layer->polygons); + } + Slic3r::polygons_append(*m_polygons_to_extrude, std::move(*other.m_polygons_to_extrude)); + *m_polygons_to_extrude = union_safety_offset(*m_polygons_to_extrude); + other.m_polygons_to_extrude.reset(); + } else if (m_polygons_to_extrude != nullptr) { + assert(other.m_polygons_to_extrude == nullptr); + // The other layer has no extrusions generated yet, if it has no m_polygons_to_extrude (its area to extrude was not reduced yet). + assert(other.extrusions.empty()); + Slic3r::polygons_append(*m_polygons_to_extrude, other.layer->polygons); + *m_polygons_to_extrude = union_safety_offset(*m_polygons_to_extrude); + } + // 2) Merge the extrusions. + this->extrusions.insert(this->extrusions.end(), other.extrusions.begin(), other.extrusions.end()); + other.extrusions.clear(); + // 3) Merge the infill polygons. + Slic3r::polygons_append(this->layer->polygons, std::move(other.layer->polygons)); + this->layer->polygons = union_safety_offset(this->layer->polygons); + other.layer->polygons.clear(); + } + + void polygons_append(Polygons &dst) const { + if (layer != NULL && ! layer->polygons.empty()) + Slic3r::polygons_append(dst, layer->polygons); + } + + // The source layer. It carries the height and extrusion type (bridging / non bridging, extrusion height). + SupportGeneratorLayer *layer { nullptr }; + // Collect extrusions. They will be exported sorted by the bottom height. + ExtrusionEntitiesPtr extrusions; + +private: + // In case the extrusions are non-empty, m_polygons_to_extrude may contain the rest areas yet to be filled by additional support. + // This is useful mainly for the loop interfaces, which are generated before the zig-zag infills. + std::unique_ptr m_polygons_to_extrude; +}; + +typedef std::vector SupportGeneratorLayerExtrudedPtrs; + +struct LoopInterfaceProcessor +{ + LoopInterfaceProcessor(coordf_t circle_r) : + n_contact_loops(0), + circle_radius(circle_r), + circle_distance(circle_r * 3.) + { + // Shape of the top contact area. + circle.points.reserve(6); + for (size_t i = 0; i < 6; ++ i) { + double angle = double(i) * M_PI / 3.; + circle.points.push_back(Point(circle_radius * cos(angle), circle_radius * sin(angle))); + } + } + + // Generate loop contacts at the top_contact_layer, + // trim the top_contact_layer->polygons with the areas covered by the loops. + void generate(SupportGeneratorLayerExtruded &top_contact_layer, const Flow &interface_flow_src) const; + + int n_contact_loops; + coordf_t circle_radius; + coordf_t circle_distance; + Polygon circle; +}; + +void LoopInterfaceProcessor::generate(SupportGeneratorLayerExtruded &top_contact_layer, const Flow &interface_flow_src) const +{ + if (n_contact_loops == 0 || top_contact_layer.empty()) + return; + + Flow flow = interface_flow_src.with_height(top_contact_layer.layer->height); + + Polygons overhang_polygons; + if (top_contact_layer.layer->overhang_polygons != nullptr) + overhang_polygons = std::move(*top_contact_layer.layer->overhang_polygons); + + // Generate the outermost loop. + // Find centerline of the external loop (or any other kind of extrusions should the loop be skipped) + ExPolygons top_contact_expolygons = offset_ex(union_ex(top_contact_layer.layer->polygons), - 0.5f * flow.scaled_width()); + + // Grid size and bit shifts for quick and exact to/from grid coordinates manipulation. + coord_t circle_grid_resolution = 1; + coord_t circle_grid_powerof2 = 0; + { + // epsilon to account for rounding errors + coord_t circle_grid_resolution_non_powerof2 = coord_t(2. * circle_distance + 3.); + while (circle_grid_resolution < circle_grid_resolution_non_powerof2) { + circle_grid_resolution <<= 1; + ++ circle_grid_powerof2; + } + } + + struct PointAccessor { + const Point* operator()(const Point &pt) const { return &pt; } + }; + typedef ClosestPointInRadiusLookup ClosestPointLookupType; + + Polygons loops0; + { + // find centerline of the external loop of the contours + // Only consider the loops facing the overhang. + Polygons external_loops; + // Holes in the external loops. + Polygons circles; + Polygons overhang_with_margin = offset(union_ex(overhang_polygons), 0.5f * flow.scaled_width()); + for (ExPolygons::iterator it_contact_expoly = top_contact_expolygons.begin(); it_contact_expoly != top_contact_expolygons.end(); ++ it_contact_expoly) { + // Store the circle centers placed for an expolygon into a regular grid, hashed by the circle centers. + ClosestPointLookupType circle_centers_lookup(coord_t(circle_distance - SCALED_EPSILON)); + Points circle_centers; + Point center_last; + // For each contour of the expolygon, start with the outer contour, continue with the holes. + for (size_t i_contour = 0; i_contour <= it_contact_expoly->holes.size(); ++ i_contour) { + Polygon &contour = (i_contour == 0) ? it_contact_expoly->contour : it_contact_expoly->holes[i_contour - 1]; + const Point *seg_current_pt = nullptr; + coordf_t seg_current_t = 0.; + if (! intersection_pl(contour.split_at_first_point(), overhang_with_margin).empty()) { + // The contour is below the overhang at least to some extent. + //FIXME ideally one would place the circles below the overhang only. + // Walk around the contour and place circles so their centers are not closer than circle_distance from each other. + if (circle_centers.empty()) { + // Place the first circle. + seg_current_pt = &contour.points.front(); + seg_current_t = 0.; + center_last = *seg_current_pt; + circle_centers_lookup.insert(center_last); + circle_centers.push_back(center_last); + } + for (Points::const_iterator it = contour.points.begin() + 1; it != contour.points.end(); ++it) { + // Is it possible to place a circle on this segment? Is it not too close to any of the circles already placed on this contour? + const Point &p1 = *(it-1); + const Point &p2 = *it; + // Intersection of a ray (p1, p2) with a circle placed at center_last, with radius of circle_distance. + const Vec2d v_seg(coordf_t(p2(0)) - coordf_t(p1(0)), coordf_t(p2(1)) - coordf_t(p1(1))); + const Vec2d v_cntr(coordf_t(p1(0) - center_last(0)), coordf_t(p1(1) - center_last(1))); + coordf_t a = v_seg.squaredNorm(); + coordf_t b = 2. * v_seg.dot(v_cntr); + coordf_t c = v_cntr.squaredNorm() - circle_distance * circle_distance; + coordf_t disc = b * b - 4. * a * c; + if (disc > 0.) { + // The circle intersects a ray. Avoid the parts of the segment inside the circle. + coordf_t t1 = (-b - sqrt(disc)) / (2. * a); + coordf_t t2 = (-b + sqrt(disc)) / (2. * a); + coordf_t t0 = (seg_current_pt == &p1) ? seg_current_t : 0.; + // Take the lowest t in , excluding . + coordf_t t; + if (t0 <= t1) + t = t0; + else if (t2 <= 1.) + t = t2; + else { + // Try the following segment. + seg_current_pt = nullptr; + continue; + } + seg_current_pt = &p1; + seg_current_t = t; + center_last = Point(p1(0) + coord_t(v_seg(0) * t), p1(1) + coord_t(v_seg(1) * t)); + // It has been verified that the new point is far enough from center_last. + // Ensure, that it is far enough from all the centers. + std::pair circle_closest = circle_centers_lookup.find(center_last); + if (circle_closest.first != nullptr) { + -- it; + continue; + } + } else { + // All of the segment is outside the circle. Take the first point. + seg_current_pt = &p1; + seg_current_t = 0.; + center_last = p1; + } + // Place the first circle. + circle_centers_lookup.insert(center_last); + circle_centers.push_back(center_last); + } + external_loops.push_back(std::move(contour)); + for (const Point ¢er : circle_centers) { + circles.push_back(circle); + circles.back().translate(center); + } + } + } + } + // Apply a pattern to the external loops. + loops0 = diff(external_loops, circles); + } + + Polylines loop_lines; + { + // make more loops + Polygons loop_polygons = loops0; + for (int i = 1; i < n_contact_loops; ++ i) + polygons_append(loop_polygons, + opening( + loops0, + i * flow.scaled_spacing() + 0.5f * flow.scaled_spacing(), + 0.5f * flow.scaled_spacing())); + // Clip such loops to the side oriented towards the object. + // Collect split points, so they will be recognized after the clipping. + // At the split points the clipped pieces will be stitched back together. + loop_lines.reserve(loop_polygons.size()); + std::unordered_map map_split_points; + for (Polygons::const_iterator it = loop_polygons.begin(); it != loop_polygons.end(); ++ it) { + assert(map_split_points.find(it->first_point()) == map_split_points.end()); + map_split_points[it->first_point()] = -1; + loop_lines.push_back(it->split_at_first_point()); + } + loop_lines = intersection_pl(loop_lines, expand(overhang_polygons, scale_(SUPPORT_MATERIAL_MARGIN))); + // Because a closed loop has been split to a line, loop_lines may contain continuous segments split to 2 pieces. + // Try to connect them. + for (int i_line = 0; i_line < int(loop_lines.size()); ++ i_line) { + Polyline &polyline = loop_lines[i_line]; + auto it = map_split_points.find(polyline.first_point()); + if (it != map_split_points.end()) { + // This is a stitching point. + // If this assert triggers, multiple source polygons likely intersected at this point. + assert(it->second != -2); + if (it->second < 0) { + // First occurence. + it->second = i_line; + } else { + // Second occurence. Join the lines. + Polyline &polyline_1st = loop_lines[it->second]; + assert(polyline_1st.first_point() == it->first || polyline_1st.last_point() == it->first); + if (polyline_1st.first_point() == it->first) + polyline_1st.reverse(); + polyline_1st.append(std::move(polyline)); + it->second = -2; + } + continue; + } + it = map_split_points.find(polyline.last_point()); + if (it != map_split_points.end()) { + // This is a stitching point. + // If this assert triggers, multiple source polygons likely intersected at this point. + assert(it->second != -2); + if (it->second < 0) { + // First occurence. + it->second = i_line; + } else { + // Second occurence. Join the lines. + Polyline &polyline_1st = loop_lines[it->second]; + assert(polyline_1st.first_point() == it->first || polyline_1st.last_point() == it->first); + if (polyline_1st.first_point() == it->first) + polyline_1st.reverse(); + polyline.reverse(); + polyline_1st.append(std::move(polyline)); + it->second = -2; + } + } + } + // Remove empty lines. + remove_degenerate(loop_lines); + } + + // add the contact infill area to the interface area + // note that growing loops by $circle_radius ensures no tiny + // extrusions are left inside the circles; however it creates + // a very large gap between loops and contact_infill_polygons, so maybe another + // solution should be found to achieve both goals + // Store the trimmed polygons into a separate polygon set, so the original infill area remains intact for + // "modulate by layer thickness". + top_contact_layer.set_polygons_to_extrude(diff(top_contact_layer.layer->polygons, offset(loop_lines, float(circle_radius * 1.1)))); + + // Transform loops into ExtrusionPath objects. + extrusion_entities_append_paths( + top_contact_layer.extrusions, + std::move(loop_lines), + ExtrusionRole::SupportMaterialInterface, flow.mm3_per_mm(), flow.width(), flow.height()); +} + +#ifdef SLIC3R_DEBUG +static std::string dbg_index_to_color(int idx) +{ + if (idx < 0) + return "yellow"; + idx = idx % 3; + switch (idx) { + case 0: return "red"; + case 1: return "green"; + default: return "blue"; + } +} +#endif /* SLIC3R_DEBUG */ + +// When extruding a bottom interface layer over an object, the bottom interface layer is extruded in a thin air, therefore +// it is being extruded with a bridging flow to not shrink excessively (the die swell effect). +// Tiny extrusions are better avoided and it is always better to anchor the thread to an existing support structure if possible. +// Therefore the bottom interface spots are expanded a bit. The expanded regions may overlap with another bottom interface layers, +// leading to over extrusion, where they overlap. The over extrusion is better avoided as it often makes the interface layers +// to stick too firmly to the object. +// +// Modulate thickness (increase bottom_z) of extrusions_in_out generated for this_layer +// if they overlap with overlapping_layers, whose print_z is above this_layer.bottom_z() and below this_layer.print_z. +static void modulate_extrusion_by_overlapping_layers( + // Extrusions generated for this_layer. + ExtrusionEntitiesPtr &extrusions_in_out, + const SupportGeneratorLayer &this_layer, + // Multiple layers overlapping with this_layer, sorted bottom up. + const SupportGeneratorLayersPtr &overlapping_layers) +{ + size_t n_overlapping_layers = overlapping_layers.size(); + if (n_overlapping_layers == 0 || extrusions_in_out.empty()) + // The extrusions do not overlap with any other extrusion. + return; + + // Get the initial extrusion parameters. + ExtrusionPath *extrusion_path_template = dynamic_cast(extrusions_in_out.front()); + assert(extrusion_path_template != nullptr); + ExtrusionRole extrusion_role = extrusion_path_template->role(); + float extrusion_width = extrusion_path_template->width; + + struct ExtrusionPathFragment + { + ExtrusionPathFragment() : mm3_per_mm(-1), width(-1), height(-1) {}; + ExtrusionPathFragment(double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height) {}; + + Polylines polylines; + double mm3_per_mm; + float width; + float height; + }; + + // Split the extrusions by the overlapping layers, reduce their extrusion rate. + // The last path_fragment is from this_layer. + std::vector path_fragments( + n_overlapping_layers + 1, + ExtrusionPathFragment(extrusion_path_template->mm3_per_mm, extrusion_path_template->width, extrusion_path_template->height)); + // Don't use it, it will be released. + extrusion_path_template = nullptr; + +#ifdef SLIC3R_DEBUG + static int iRun = 0; + ++ iRun; + BoundingBox bbox; + for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { + const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; + bbox.merge(get_extents(overlapping_layer.polygons)); + } + for (ExtrusionEntitiesPtr::const_iterator it = extrusions_in_out.begin(); it != extrusions_in_out.end(); ++ it) { + ExtrusionPath *path = dynamic_cast(*it); + assert(path != nullptr); + bbox.merge(get_extents(path->polyline)); + } + SVG svg(debug_out_path("support-fragments-%d-%lf.svg", iRun, this_layer.print_z).c_str(), bbox); + const float transparency = 0.5f; + // Filled polygons for the overlapping regions. + svg.draw(union_ex(this_layer.polygons), dbg_index_to_color(-1), transparency); + for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { + const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; + svg.draw(union_ex(overlapping_layer.polygons), dbg_index_to_color(int(i_overlapping_layer)), transparency); + } + // Contours of the overlapping regions. + svg.draw(to_polylines(this_layer.polygons), dbg_index_to_color(-1), scale_(0.2)); + for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { + const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; + svg.draw(to_polylines(overlapping_layer.polygons), dbg_index_to_color(int(i_overlapping_layer)), scale_(0.1)); + } + // Fill extrusion, the source. + for (ExtrusionEntitiesPtr::const_iterator it = extrusions_in_out.begin(); it != extrusions_in_out.end(); ++ it) { + ExtrusionPath *path = dynamic_cast(*it); + std::string color_name; + switch ((it - extrusions_in_out.begin()) % 9) { + case 0: color_name = "magenta"; break; + case 1: color_name = "deepskyblue"; break; + case 2: color_name = "coral"; break; + case 3: color_name = "goldenrod"; break; + case 4: color_name = "orange"; break; + case 5: color_name = "olivedrab"; break; + case 6: color_name = "blueviolet"; break; + case 7: color_name = "brown"; break; + default: color_name = "orchid"; break; + } + svg.draw(path->polyline, color_name, scale_(0.2)); + } +#endif /* SLIC3R_DEBUG */ + + // End points of the original paths. + std::vector> path_ends; + // Collect the paths of this_layer. + { + Polylines &polylines = path_fragments.back().polylines; + for (ExtrusionEntity *ee : extrusions_in_out) { + ExtrusionPath *path = dynamic_cast(ee); + assert(path != nullptr); + polylines.emplace_back(Polyline(std::move(path->polyline))); + path_ends.emplace_back(std::pair(polylines.back().points.front(), polylines.back().points.back())); + delete path; + } + } + // Destroy the original extrusion paths, their polylines were moved to path_fragments already. + // This will be the destination for the new paths. + extrusions_in_out.clear(); + + // Fragment the path segments by overlapping layers. The overlapping layers are sorted by an increasing print_z. + // Trim by the highest overlapping layer first. + for (int i_overlapping_layer = int(n_overlapping_layers) - 1; i_overlapping_layer >= 0; -- i_overlapping_layer) { + const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; + ExtrusionPathFragment &frag = path_fragments[i_overlapping_layer]; + Polygons polygons_trimming = offset(union_ex(overlapping_layer.polygons), float(scale_(0.5*extrusion_width))); + frag.polylines = intersection_pl(path_fragments.back().polylines, polygons_trimming); + path_fragments.back().polylines = diff_pl(path_fragments.back().polylines, polygons_trimming); + // Adjust the extrusion parameters for a reduced layer height and a non-bridging flow (nozzle_dmr = -1, does not matter). + assert(this_layer.print_z > overlapping_layer.print_z); + frag.height = float(this_layer.print_z - overlapping_layer.print_z); + frag.mm3_per_mm = Flow(frag.width, frag.height, -1.f).mm3_per_mm(); +#ifdef SLIC3R_DEBUG + svg.draw(frag.polylines, dbg_index_to_color(i_overlapping_layer), scale_(0.1)); +#endif /* SLIC3R_DEBUG */ + } + +#ifdef SLIC3R_DEBUG + svg.draw(path_fragments.back().polylines, dbg_index_to_color(-1), scale_(0.1)); + svg.Close(); +#endif /* SLIC3R_DEBUG */ + + // Now chain the split segments using hashing and a nearly exact match, maintaining the order of segments. + // Create a single ExtrusionPath or ExtrusionEntityCollection per source ExtrusionPath. + // Map of fragment start/end points to a pair of + // Because a non-exact matching is used for the end points, a multi-map is used. + // As the clipper library may reverse the order of some clipped paths, store both ends into the map. + struct ExtrusionPathFragmentEnd + { + ExtrusionPathFragmentEnd(size_t alayer_idx, size_t apolyline_idx, bool ais_start) : + layer_idx(alayer_idx), polyline_idx(apolyline_idx), is_start(ais_start) {} + size_t layer_idx; + size_t polyline_idx; + bool is_start; + }; + class ExtrusionPathFragmentEndPointAccessor { + public: + ExtrusionPathFragmentEndPointAccessor(const std::vector &path_fragments) : m_path_fragments(path_fragments) {} + // Return an end point of a fragment, or nullptr if the fragment has been consumed already. + const Point* operator()(const ExtrusionPathFragmentEnd &fragment_end) const { + const Polyline &polyline = m_path_fragments[fragment_end.layer_idx].polylines[fragment_end.polyline_idx]; + return polyline.points.empty() ? nullptr : + (fragment_end.is_start ? &polyline.points.front() : &polyline.points.back()); + } + private: + ExtrusionPathFragmentEndPointAccessor& operator=(const ExtrusionPathFragmentEndPointAccessor&) { + return *this; + } + + const std::vector &m_path_fragments; + }; + const coord_t search_radius = 7; + ClosestPointInRadiusLookup map_fragment_starts( + search_radius, ExtrusionPathFragmentEndPointAccessor(path_fragments)); + for (size_t i_overlapping_layer = 0; i_overlapping_layer <= n_overlapping_layers; ++ i_overlapping_layer) { + const Polylines &polylines = path_fragments[i_overlapping_layer].polylines; + for (size_t i_polyline = 0; i_polyline < polylines.size(); ++ i_polyline) { + // Map a starting point of a polyline to a pair of + if (polylines[i_polyline].points.size() >= 2) { + map_fragment_starts.insert(ExtrusionPathFragmentEnd(i_overlapping_layer, i_polyline, true)); + map_fragment_starts.insert(ExtrusionPathFragmentEnd(i_overlapping_layer, i_polyline, false)); + } + } + } + + // For each source path: + for (size_t i_path = 0; i_path < path_ends.size(); ++ i_path) { + const Point &pt_start = path_ends[i_path].first; + const Point &pt_end = path_ends[i_path].second; + Point pt_current = pt_start; + // Find a chain of fragments with the original / reduced print height. + ExtrusionMultiPath multipath; + for (;;) { + // Find a closest end point to pt_current. + std::pair end_and_dist2 = map_fragment_starts.find(pt_current); + // There may be a bug in Clipper flipping the order of two last points in a fragment? + // assert(end_and_dist2.first != nullptr); + assert(end_and_dist2.first == nullptr || end_and_dist2.second < search_radius * search_radius); + if (end_and_dist2.first == nullptr) { + // New fragment connecting to pt_current was not found. + // Verify that the last point found is close to the original end point of the unfragmented path. + //const double d2 = (pt_end - pt_current).cast.squaredNorm(); + //assert(d2 < coordf_t(search_radius * search_radius)); + // End of the path. + break; + } + const ExtrusionPathFragmentEnd &fragment_end_min = *end_and_dist2.first; + // Fragment to consume. + ExtrusionPathFragment &frag = path_fragments[fragment_end_min.layer_idx]; + Polyline &frag_polyline = frag.polylines[fragment_end_min.polyline_idx]; + // Path to append the fragment to. + ExtrusionPath *path = multipath.paths.empty() ? nullptr : &multipath.paths.back(); + if (path != nullptr) { + // Verify whether the path is compatible with the current fragment. + assert(this_layer.layer_type == SupporLayerType::BottomContact || path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm); + if (path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm) { + path = nullptr; + } + // Merging with the previous path. This can only happen if the current layer was reduced by a base layer, which was split into a base and interface layer. + } + if (path == nullptr) { + // Allocate a new path. + multipath.paths.push_back(ExtrusionPath(extrusion_role, frag.mm3_per_mm, frag.width, frag.height)); + path = &multipath.paths.back(); + } + // The Clipper library may flip the order of the clipped polylines arbitrarily. + // Reverse the source polyline, if connecting to the end. + if (! fragment_end_min.is_start) + frag_polyline.reverse(); + // Enforce exact overlap of the end points of successive fragments. + assert(frag_polyline.points.front() == pt_current); + frag_polyline.points.front() = pt_current; + // Don't repeat the first point. + if (! path->polyline.points.empty()) + path->polyline.points.pop_back(); + // Consume the fragment's polyline, remove it from the input fragments, so it will be ignored the next time. + path->polyline.append(std::move(frag_polyline)); + frag_polyline.points.clear(); + pt_current = path->polyline.points.back(); + if (pt_current == pt_end) { + // End of the path. + break; + } + } + if (!multipath.paths.empty()) { + if (multipath.paths.size() == 1) { + // This path was not fragmented. + extrusions_in_out.push_back(new ExtrusionPath(std::move(multipath.paths.front()))); + } else { + // This path was fragmented. Copy the collection as a whole object, so the order inside the collection will not be changed + // during the chaining of extrusions_in_out. + extrusions_in_out.push_back(new ExtrusionMultiPath(std::move(multipath))); + } + } + } + // If there are any non-consumed fragments, add them separately. + //FIXME this shall not happen, if the Clipper works as expected and all paths split to fragments could be re-connected. + for (auto it_fragment = path_fragments.begin(); it_fragment != path_fragments.end(); ++ it_fragment) + extrusion_entities_append_paths(extrusions_in_out, std::move(it_fragment->polylines), extrusion_role, it_fragment->mm3_per_mm, it_fragment->width, it_fragment->height); +} + +// Support layer that is covered by some form of dense interface. +static constexpr const std::initializer_list support_types_interface{ + SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface +}; + +SupportGeneratorLayersPtr generate_support_layers( + PrintObject &object, + const SupportGeneratorLayersPtr &raft_layers, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &intermediate_layers, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers) +{ + // Install support layers into the object. + // A support layer installed on a PrintObject has a unique print_z. + SupportGeneratorLayersPtr layers_sorted; + layers_sorted.reserve(raft_layers.size() + bottom_contacts.size() + top_contacts.size() + intermediate_layers.size() + interface_layers.size() + base_interface_layers.size()); + append(layers_sorted, raft_layers); + append(layers_sorted, bottom_contacts); + append(layers_sorted, top_contacts); + append(layers_sorted, intermediate_layers); + append(layers_sorted, interface_layers); + append(layers_sorted, base_interface_layers); + // Sort the layers lexicographically by a raising print_z and a decreasing height. + std::sort(layers_sorted.begin(), layers_sorted.end(), [](auto *l1, auto *l2) { return *l1 < *l2; }); + int layer_id = 0; + int layer_id_interface = 0; + assert(object.support_layers().empty()); + for (size_t i = 0; i < layers_sorted.size();) { + // Find the last layer with roughly the same print_z, find the minimum layer height of all. + // Due to the floating point inaccuracies, the print_z may not be the same even if in theory they should. + size_t j = i + 1; + coordf_t zmax = layers_sorted[i]->print_z + EPSILON; + for (; j < layers_sorted.size() && layers_sorted[j]->print_z <= zmax; ++j) ; + // Assign an average print_z to the set of layers with nearly equal print_z. + coordf_t zavg = 0.5 * (layers_sorted[i]->print_z + layers_sorted[j - 1]->print_z); + coordf_t height_min = layers_sorted[i]->height; + bool empty = true; + // For snug supports, layers where the direction of the support interface shall change are accounted for. + size_t num_interfaces = 0; + size_t num_top_contacts = 0; + double top_contact_bottom_z = 0; + for (size_t u = i; u < j; ++u) { + SupportGeneratorLayer &layer = *layers_sorted[u]; + if (! layer.polygons.empty()) { + empty = false; + num_interfaces += one_of(layer.layer_type, support_types_interface); + if (layer.layer_type == SupporLayerType::TopContact) { + ++ num_top_contacts; + assert(num_top_contacts <= 1); + // All top contact layers sharing this print_z shall also share bottom_z. + //assert(num_top_contacts == 1 || (top_contact_bottom_z - layer.bottom_z) < EPSILON); + top_contact_bottom_z = layer.bottom_z; + } + } + layer.print_z = zavg; + height_min = std::min(height_min, layer.height); + } + if (! empty) { + // Here the upper_layer and lower_layer pointers are left to null at the support layers, + // as they are never used. These pointers are candidates for removal. + bool this_layer_contacts_only = num_top_contacts > 0 && num_top_contacts == num_interfaces; + size_t this_layer_id_interface = layer_id_interface; + if (this_layer_contacts_only) { + // Find a supporting layer for its interface ID. + for (auto it = object.support_layers().rbegin(); it != object.support_layers().rend(); ++ it) + if (const SupportLayer &other_layer = **it; std::abs(other_layer.print_z - top_contact_bottom_z) < EPSILON) { + // other_layer supports this top contact layer. Assign a different support interface direction to this layer + // from the layer that supports it. + this_layer_id_interface = other_layer.interface_id() + 1; + } + } + object.add_support_layer(layer_id ++, this_layer_id_interface, height_min, zavg); + if (num_interfaces && ! this_layer_contacts_only) + ++ layer_id_interface; + } + i = j; + } + return layers_sorted; +} + +void generate_support_toolpaths( + SupportLayerPtrs &support_layers, + const PrintObjectConfig &config, + const SupportParameters &support_params, + const SlicingParameters &slicing_params, + const SupportGeneratorLayersPtr &raft_layers, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &intermediate_layers, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers) +{ + // loop_interface_processor with a given circle radius. + LoopInterfaceProcessor loop_interface_processor(1.5 * support_params.support_material_interface_flow.scaled_width()); + loop_interface_processor.n_contact_loops = config.support_material_interface_contact_loops ? 1 : 0; + + std::vector angles { support_params.base_angle }; + if (config.support_material_pattern == smpRectilinearGrid) + angles.push_back(support_params.interface_angle); + + BoundingBox bbox_object(Point(-scale_(1.), -scale_(1.0)), Point(scale_(1.), scale_(1.))); + +// const coordf_t link_max_length_factor = 3.; + const coordf_t link_max_length_factor = 0.; + + // Insert the raft base layers. + auto n_raft_layers = std::min(support_layers.size(), std::max(0, int(slicing_params.raft_layers()) - 1)); + + tbb::parallel_for(tbb::blocked_range(0, n_raft_layers), + [&support_layers, &raft_layers, &intermediate_layers, &config, &support_params, &slicing_params, + &bbox_object, link_max_length_factor] + (const tbb::blocked_range& range) { + for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) + { + assert(support_layer_id < raft_layers.size()); + SupportLayer &support_layer = *support_layers[support_layer_id]; + assert(support_layer.support_fills.entities.empty()); + SupportGeneratorLayer &raft_layer = *raft_layers[support_layer_id]; + + std::unique_ptr filler_interface = std::unique_ptr(Fill::new_from_type(support_params.raft_interface_fill_pattern)); + std::unique_ptr filler_support = std::unique_ptr(Fill::new_from_type(support_params.base_fill_pattern)); + filler_interface->set_bounding_box(bbox_object); + filler_support->set_bounding_box(bbox_object); + + // Print the tree supports cutting through the raft with the exception of the 1st layer, where a full support layer will be printed below + // both the raft and the trees. + // Trim the raft layers with the tree polygons. + const Polygons &tree_polygons = + support_layer_id > 0 && support_layer_id < intermediate_layers.size() && is_approx(intermediate_layers[support_layer_id]->print_z, support_layer.print_z) ? + intermediate_layers[support_layer_id]->polygons : Polygons(); + + // Print the support base below the support columns, or the support base for the support columns plus the contacts. + if (support_layer_id > 0) { + const Polygons &to_infill_polygons = (support_layer_id < slicing_params.base_raft_layers) ? + raft_layer.polygons : + //FIXME misusing contact_polygons for support columns. + ((raft_layer.contact_polygons == nullptr) ? Polygons() : *raft_layer.contact_polygons); + // Trees may cut through the raft layers down to a print bed. + Flow flow(float(support_params.support_material_flow.width()), float(raft_layer.height), support_params.support_material_flow.nozzle_diameter()); + assert(!raft_layer.bridging); + if (! to_infill_polygons.empty()) { + Fill *filler = filler_support.get(); + filler->angle = support_params.raft_angle_base; + filler->spacing = support_params.support_material_flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.support_density)); + fill_expolygons_with_sheath_generate_paths( + // Destination + support_layer.support_fills.entities, + // Regions to fill + tree_polygons.empty() ? to_infill_polygons : diff(to_infill_polygons, tree_polygons), + // Filler and its parameters + filler, float(support_params.support_density), + // Extrusion parameters + ExtrusionRole::SupportMaterial, flow, + support_params.with_sheath, false); + } + if (! tree_polygons.empty()) + tree_supports_generate_paths(support_layer.support_fills.entities, tree_polygons, flow); + } + + Fill *filler = filler_interface.get(); + Flow flow = support_params.first_layer_flow; + float density = 0.f; + if (support_layer_id == 0) { + // Base flange. + filler->angle = support_params.raft_angle_1st_layer; + filler->spacing = support_params.first_layer_flow.spacing(); + density = float(config.raft_first_layer_density.value * 0.01); + } else if (support_layer_id >= slicing_params.base_raft_layers) { + filler->angle = support_params.raft_interface_angle(support_layer.interface_id()); + // We don't use $base_flow->spacing because we need a constant spacing + // value that guarantees that all layers are correctly aligned. + filler->spacing = support_params.support_material_flow.spacing(); + assert(! raft_layer.bridging); + flow = Flow(float(support_params.raft_interface_flow.width()), float(raft_layer.height), support_params.raft_interface_flow.nozzle_diameter()); + density = float(support_params.raft_interface_density); + } else + continue; + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); + fill_expolygons_with_sheath_generate_paths( + // Destination + support_layer.support_fills.entities, + // Regions to fill + tree_polygons.empty() ? raft_layer.polygons : diff(raft_layer.polygons, tree_polygons), + // Filler and its parameters + filler, density, + // Extrusion parameters + (support_layer_id < slicing_params.base_raft_layers) ? ExtrusionRole::SupportMaterial : ExtrusionRole::SupportMaterialInterface, flow, + // sheath at first layer + support_layer_id == 0, support_layer_id == 0); + } + }); + + struct LayerCacheItem { + LayerCacheItem(SupportGeneratorLayerExtruded *layer_extruded = nullptr) : layer_extruded(layer_extruded) {} + SupportGeneratorLayerExtruded *layer_extruded; + std::vector overlapping; + }; + struct LayerCache { + SupportGeneratorLayerExtruded bottom_contact_layer; + SupportGeneratorLayerExtruded top_contact_layer; + SupportGeneratorLayerExtruded base_layer; + SupportGeneratorLayerExtruded interface_layer; + SupportGeneratorLayerExtruded base_interface_layer; + boost::container::static_vector nonempty; + + void add_nonempty_and_sort() { + for (SupportGeneratorLayerExtruded *item : { &bottom_contact_layer, &top_contact_layer, &interface_layer, &base_interface_layer, &base_layer }) + if (! item->empty()) + this->nonempty.emplace_back(item); + // Sort the layers with the same print_z coordinate by their heights, thickest first. + std::stable_sort(this->nonempty.begin(), this->nonempty.end(), [](const LayerCacheItem &lc1, const LayerCacheItem &lc2) { return lc1.layer_extruded->layer->height > lc2.layer_extruded->layer->height; }); + } + }; + std::vector layer_caches(support_layers.size()); + + tbb::parallel_for(tbb::blocked_range(n_raft_layers, support_layers.size()), + [&config, &slicing_params, &support_params, &support_layers, &bottom_contacts, &top_contacts, &intermediate_layers, &interface_layers, &base_interface_layers, &layer_caches, &loop_interface_processor, + &bbox_object, &angles, n_raft_layers, link_max_length_factor] + (const tbb::blocked_range& range) { + // Indices of the 1st layer in their respective container at the support layer height. + size_t idx_layer_bottom_contact = size_t(-1); + size_t idx_layer_top_contact = size_t(-1); + size_t idx_layer_intermediate = size_t(-1); + size_t idx_layer_interface = size_t(-1); + size_t idx_layer_base_interface = size_t(-1); + const auto fill_type_first_layer = ipRectilinear; + auto filler_interface = std::unique_ptr(Fill::new_from_type(support_params.contact_fill_pattern)); + // Filler for the 1st layer interface, if different from filler_interface. + auto filler_first_layer_ptr = std::unique_ptr(range.begin() == 0 && support_params.contact_fill_pattern != fill_type_first_layer ? Fill::new_from_type(fill_type_first_layer) : nullptr); + // Pointer to the 1st layer interface filler. + auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get(); + // Filler for the 1st layer interface, if different from filler_interface. + auto filler_raft_contact_ptr = std::unique_ptr(range.begin() == n_raft_layers && config.support_material_interface_layers.value == 0 ? + Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr); + // Pointer to the 1st layer interface filler. + auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get(); + // Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer). + auto filler_base_interface = std::unique_ptr(base_interface_layers.empty() ? nullptr : + Fill::new_from_type(support_params.interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase)); + auto filler_support = std::unique_ptr(Fill::new_from_type(support_params.base_fill_pattern)); + filler_interface->set_bounding_box(bbox_object); + if (filler_first_layer_ptr) + filler_first_layer_ptr->set_bounding_box(bbox_object); + if (filler_raft_contact_ptr) + filler_raft_contact_ptr->set_bounding_box(bbox_object); + if (filler_base_interface) + filler_base_interface->set_bounding_box(bbox_object); + filler_support->set_bounding_box(bbox_object); + for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) + { + SupportLayer &support_layer = *support_layers[support_layer_id]; + LayerCache &layer_cache = layer_caches[support_layer_id]; + const float support_interface_angle = config.support_material_style.value == smsGrid ? + support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()); + + // Find polygons with the same print_z. + SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer; + SupportGeneratorLayerExtruded &top_contact_layer = layer_cache.top_contact_layer; + SupportGeneratorLayerExtruded &base_layer = layer_cache.base_layer; + SupportGeneratorLayerExtruded &interface_layer = layer_cache.interface_layer; + SupportGeneratorLayerExtruded &base_interface_layer = layer_cache.base_interface_layer; + // Increment the layer indices to find a layer at support_layer.print_z. + { + auto fun = [&support_layer](const SupportGeneratorLayer *l){ return l->print_z >= support_layer.print_z - EPSILON; }; + idx_layer_bottom_contact = idx_higher_or_equal(bottom_contacts, idx_layer_bottom_contact, fun); + idx_layer_top_contact = idx_higher_or_equal(top_contacts, idx_layer_top_contact, fun); + idx_layer_intermediate = idx_higher_or_equal(intermediate_layers, idx_layer_intermediate, fun); + idx_layer_interface = idx_higher_or_equal(interface_layers, idx_layer_interface, fun); + idx_layer_base_interface = idx_higher_or_equal(base_interface_layers, idx_layer_base_interface,fun); + } + // Copy polygons from the layers. + if (idx_layer_bottom_contact < bottom_contacts.size() && bottom_contacts[idx_layer_bottom_contact]->print_z < support_layer.print_z + EPSILON) + bottom_contact_layer.layer = bottom_contacts[idx_layer_bottom_contact]; + if (idx_layer_top_contact < top_contacts.size() && top_contacts[idx_layer_top_contact]->print_z < support_layer.print_z + EPSILON) + top_contact_layer.layer = top_contacts[idx_layer_top_contact]; + if (idx_layer_interface < interface_layers.size() && interface_layers[idx_layer_interface]->print_z < support_layer.print_z + EPSILON) + interface_layer.layer = interface_layers[idx_layer_interface]; + if (idx_layer_base_interface < base_interface_layers.size() && base_interface_layers[idx_layer_base_interface]->print_z < support_layer.print_z + EPSILON) + base_interface_layer.layer = base_interface_layers[idx_layer_base_interface]; + if (idx_layer_intermediate < intermediate_layers.size() && intermediate_layers[idx_layer_intermediate]->print_z < support_layer.print_z + EPSILON) + base_layer.layer = intermediate_layers[idx_layer_intermediate]; + + // This layer is a raft contact layer. Any contact polygons at this layer are raft contacts. + bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z); + if (config.support_material_interface_layers == 0) { + // If no top interface layers were requested, we treat the contact layer exactly as a generic base layer. + // Don't merge the raft contact layer though. + if (support_params.can_merge_support_regions && ! raft_layer) { + if (base_layer.could_merge(top_contact_layer)) + base_layer.merge(std::move(top_contact_layer)); + else if (base_layer.empty()) + base_layer = std::move(top_contact_layer); + } + } else { + loop_interface_processor.generate(top_contact_layer, support_params.support_material_interface_flow); + // If no loops are allowed, we treat the contact layer exactly as a generic interface layer. + // Merge interface_layer into top_contact_layer, as the top_contact_layer is not synchronized and therefore it will be used + // to trim other layers. + if (top_contact_layer.could_merge(interface_layer) && ! raft_layer) + top_contact_layer.merge(std::move(interface_layer)); + } + if ((config.support_material_interface_layers == 0 || config.support_material_bottom_interface_layers == 0) && support_params.can_merge_support_regions) { + if (base_layer.could_merge(bottom_contact_layer)) + base_layer.merge(std::move(bottom_contact_layer)); + else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging) + base_layer = std::move(bottom_contact_layer); + } else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) + top_contact_layer.merge(std::move(bottom_contact_layer)); + else if (bottom_contact_layer.could_merge(interface_layer)) + bottom_contact_layer.merge(std::move(interface_layer)); + +#if 0 + if ( ! interface_layer.empty() && ! base_layer.empty()) { + // turn base support into interface when it's contained in our holes + // (this way we get wider interface anchoring) + //FIXME The intention of the code below is unclear. One likely wanted to just merge small islands of base layers filling in the holes + // inside interface layers, but the code below fills just too much, see GH #4570 + Polygons islands = top_level_islands(interface_layer.layer->polygons); + polygons_append(interface_layer.layer->polygons, intersection(base_layer.layer->polygons, islands)); + base_layer.layer->polygons = diff(base_layer.layer->polygons, islands); + } +#endif + + // Top and bottom contacts, interface layers. + enum class InterfaceLayerType { TopContact, BottomContact, RaftContact, Interface, InterfaceAsBase }; + auto extrude_interface = [&](SupportGeneratorLayerExtruded &layer_ex, InterfaceLayerType interface_layer_type) { + if (! layer_ex.empty() && ! layer_ex.polygons_to_extrude().empty()) { + bool interface_as_base = interface_layer_type == InterfaceLayerType::InterfaceAsBase; + bool raft_contact = interface_layer_type == InterfaceLayerType::RaftContact; + //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore + // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) + auto *filler = raft_contact ? filler_raft_contact : filler_interface.get(); + auto interface_flow = layer_ex.layer->bridging ? + Flow::bridging_flow(layer_ex.layer->height, support_params.support_material_bottom_interface_flow.nozzle_diameter()) : + (raft_contact ? &support_params.raft_interface_flow : + interface_as_base ? &support_params.support_material_flow : &support_params.support_material_interface_flow) + ->with_height(float(layer_ex.layer->height)); + filler->angle = interface_as_base ? + // If zero interface layers are configured, use the same angle as for the base layers. + angles[support_layer_id % angles.size()] : + // Use interface angle for the interface layers. + raft_contact ? + support_params.raft_interface_angle(support_layer.interface_id()) : + support_interface_angle; + double density = raft_contact ? support_params.raft_interface_density : interface_as_base ? support_params.support_density : support_params.interface_density; + filler->spacing = raft_contact ? support_params.raft_interface_flow.spacing() : + interface_as_base ? support_params.support_material_flow.spacing() : support_params.support_material_interface_flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); + fill_expolygons_generate_paths( + // Destination + layer_ex.extrusions, + // Regions to fill + union_safety_offset_ex(layer_ex.polygons_to_extrude()), + // Filler and its parameters + filler, float(density), + // Extrusion parameters + ExtrusionRole::SupportMaterialInterface, interface_flow); + } + }; + const bool top_interfaces = config.support_material_interface_layers.value != 0; + const bool bottom_interfaces = top_interfaces && config.support_material_bottom_interface_layers != 0; + extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase); + extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase); + extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase); + + // Base interface layers under soluble interfaces + if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) { + Fill *filler = filler_base_interface.get(); + //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore + // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) + assert(! base_interface_layer.layer->bridging); + Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height)); + filler->angle = support_interface_angle; + filler->spacing = support_params.support_material_interface_flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.interface_density)); + fill_expolygons_generate_paths( + // Destination + base_interface_layer.extrusions, + //base_layer_interface.extrusions, + // Regions to fill + union_safety_offset_ex(base_interface_layer.polygons_to_extrude()), + // Filler and its parameters + filler, float(support_params.interface_density), + // Extrusion parameters + ExtrusionRole::SupportMaterial, interface_flow); + } + + // Base support or flange. + if (! base_layer.empty() && ! base_layer.polygons_to_extrude().empty()) { + Fill *filler = filler_support.get(); + filler->angle = angles[support_layer_id % angles.size()]; + // We don't use $base_flow->spacing because we need a constant spacing + // value that guarantees that all layers are correctly aligned. + assert(! base_layer.layer->bridging); + auto flow = support_params.support_material_flow.with_height(float(base_layer.layer->height)); + filler->spacing = support_params.support_material_flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.support_density)); + float density = float(support_params.support_density); + bool sheath = support_params.with_sheath; + bool no_sort = false; + bool done = false; + if (base_layer.layer->bottom_z < EPSILON) { + // Base flange (the 1st layer). + filler = filler_first_layer; + filler->angle = Geometry::deg2rad(float(config.support_material_angle.value + 90.)); + density = float(config.raft_first_layer_density.value * 0.01); + flow = support_params.first_layer_flow; + // use the proper spacing for first layer as we don't need to align + // its pattern to the other layers + //FIXME When paralellizing, each thread shall have its own copy of the fillers. + filler->spacing = flow.spacing(); + filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); + sheath = true; + no_sort = true; + } else if (config.support_material_style == SupportMaterialStyle::smsOrganic) { + tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow); + done = true; + } + if (! done) + fill_expolygons_with_sheath_generate_paths( + // Destination + base_layer.extrusions, + // Regions to fill + base_layer.polygons_to_extrude(), + // Filler and its parameters + filler, density, + // Extrusion parameters + ExtrusionRole::SupportMaterial, flow, + sheath, no_sort); + } + + // Merge base_interface_layers to base_layers to avoid unneccessary retractions + if (! base_layer.empty() && ! base_interface_layer.empty() && ! base_layer.polygons_to_extrude().empty() && ! base_interface_layer.polygons_to_extrude().empty() && + base_layer.could_merge(base_interface_layer)) + base_layer.merge(std::move(base_interface_layer)); + + layer_cache.add_nonempty_and_sort(); + + // Collect the support areas with this print_z into islands, as there is no need + // for retraction over these islands. + Polygons polys; + // Collect the extrusions, sorted by the bottom extrusion height. + for (LayerCacheItem &layer_cache_item : layer_cache.nonempty) { + // Collect islands to polys. + layer_cache_item.layer_extruded->polygons_append(polys); + // The print_z of the top contact surfaces and bottom_z of the bottom contact surfaces are "free" + // in a sense that they are not synchronized with other support layers. As the top and bottom contact surfaces + // are inflated to achieve a better anchoring, it may happen, that these surfaces will at least partially + // overlap in Z with another support layers, leading to over-extrusion. + // Mitigate the over-extrusion by modulating the extrusion rate over these regions. + // The print head will follow the same print_z, but the layer thickness will be reduced + // where it overlaps with another support layer. + //FIXME When printing a briging path, what is an equivalent height of the squished extrudate of the same width? + // Collect overlapping top/bottom surfaces. + layer_cache_item.overlapping.reserve(20); + coordf_t bottom_z = layer_cache_item.layer_extruded->layer->bottom_print_z() + EPSILON; + auto add_overlapping = [&layer_cache_item, bottom_z](const SupportGeneratorLayersPtr &layers, size_t idx_top) { + for (int i = int(idx_top) - 1; i >= 0 && layers[i]->print_z > bottom_z; -- i) + layer_cache_item.overlapping.push_back(layers[i]); + }; + add_overlapping(top_contacts, idx_layer_top_contact); + if (layer_cache_item.layer_extruded->layer->layer_type == SupporLayerType::BottomContact) { + // Bottom contact layer may overlap with a base layer, which may be changed to interface layer. + add_overlapping(intermediate_layers, idx_layer_intermediate); + add_overlapping(interface_layers, idx_layer_interface); + add_overlapping(base_interface_layers, idx_layer_base_interface); + } + // Order the layers by lexicographically by an increasing print_z and a decreasing layer height. + std::stable_sort(layer_cache_item.overlapping.begin(), layer_cache_item.overlapping.end(), [](auto *l1, auto *l2) { return *l1 < *l2; }); + } + assert(support_layer.support_islands.empty()); + if (! polys.empty()) { + support_layer.support_islands = union_ex(polys); + support_layer.support_islands_bboxes.reserve(support_layer.support_islands.size()); + for (const ExPolygon &expoly : support_layer.support_islands) + support_layer.support_islands_bboxes.emplace_back(get_extents(expoly).inflated(SCALED_EPSILON)); + } + } // for each support_layer_id + }); + + // Now modulate the support layer height in parallel. + tbb::parallel_for(tbb::blocked_range(n_raft_layers, support_layers.size()), + [&support_layers, &layer_caches] + (const tbb::blocked_range& range) { + for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) { + SupportLayer &support_layer = *support_layers[support_layer_id]; + LayerCache &layer_cache = layer_caches[support_layer_id]; + // For all extrusion types at this print_z, ordered by decreasing layer height: + for (LayerCacheItem &layer_cache_item : layer_cache.nonempty) { + // Trim the extrusion height from the bottom by the overlapping layers. + modulate_extrusion_by_overlapping_layers(layer_cache_item.layer_extruded->extrusions, *layer_cache_item.layer_extruded->layer, layer_cache_item.overlapping); + support_layer.support_fills.append(std::move(layer_cache_item.layer_extruded->extrusions)); + } + } + }); + +#ifndef NDEBUG + struct Test { + static bool verify_nonempty(const ExtrusionEntityCollection *collection) { + for (const ExtrusionEntity *ee : collection->entities) { + if (const ExtrusionPath *path = dynamic_cast(ee)) + assert(! path->empty()); + else if (const ExtrusionMultiPath *multipath = dynamic_cast(ee)) + assert(! multipath->empty()); + else if (const ExtrusionEntityCollection *eecol = dynamic_cast(ee)) { + assert(! eecol->empty()); + return verify_nonempty(eecol); + } else + assert(false); + } + return true; + } + }; + for (const SupportLayer *support_layer : support_layers) + assert(Test::verify_nonempty(&support_layer->support_fills)); +#endif // NDEBUG +} + +/* +void PrintObjectSupportMaterial::clip_by_pillars( + const PrintObject &object, + LayersPtr &bottom_contacts, + LayersPtr &top_contacts, + LayersPtr &intermediate_contacts); + +{ + // this prevents supplying an empty point set to BoundingBox constructor + if (top_contacts.empty()) + return; + + coord_t pillar_size = scale_(PILLAR_SIZE); + coord_t pillar_spacing = scale_(PILLAR_SPACING); + + // A regular grid of pillars, filling the 2D bounding box. + Polygons grid; + { + // Rectangle with a side of 2.5x2.5mm. + Polygon pillar; + pillar.points.push_back(Point(0, 0)); + pillar.points.push_back(Point(pillar_size, 0)); + pillar.points.push_back(Point(pillar_size, pillar_size)); + pillar.points.push_back(Point(0, pillar_size)); + + // 2D bounding box of the projection of all contact polygons. + BoundingBox bbox; + for (LayersPtr::const_iterator it = top_contacts.begin(); it != top_contacts.end(); ++ it) + bbox.merge(get_extents((*it)->polygons)); + grid.reserve(size_t(ceil(bb.size()(0) / pillar_spacing)) * size_t(ceil(bb.size()(1) / pillar_spacing))); + for (coord_t x = bb.min(0); x <= bb.max(0) - pillar_size; x += pillar_spacing) { + for (coord_t y = bb.min(1); y <= bb.max(1) - pillar_size; y += pillar_spacing) { + grid.push_back(pillar); + for (size_t i = 0; i < pillar.points.size(); ++ i) + grid.back().points[i].translate(Point(x, y)); + } + } + } + + // add pillars to every layer + for my $i (0..n_support_z) { + $shape->[$i] = [ @$grid ]; + } + + // build capitals + for my $i (0..n_support_z) { + my $z = $support_z->[$i]; + + my $capitals = intersection( + $grid, + $contact->{$z} // [], + ); + + // work on one pillar at time (if any) to prevent the capitals from being merged + // but store the contact area supported by the capital because we need to make + // sure nothing is left + my $contact_supported_by_capitals = []; + foreach my $capital (@$capitals) { + // enlarge capital tops + $capital = offset([$capital], +($pillar_spacing - $pillar_size)/2); + push @$contact_supported_by_capitals, @$capital; + + for (my $j = $i-1; $j >= 0; $j--) { + my $jz = $support_z->[$j]; + $capital = offset($capital, -$self->interface_flow->scaled_width/2); + last if !@$capitals; + push @{ $shape->[$j] }, @$capital; + } + } + + // Capitals will not generally cover the whole contact area because there will be + // remainders. For now we handle this situation by projecting such unsupported + // areas to the ground, just like we would do with a normal support. + my $contact_not_supported_by_capitals = diff( + $contact->{$z} // [], + $contact_supported_by_capitals, + ); + if (@$contact_not_supported_by_capitals) { + for (my $j = $i-1; $j >= 0; $j--) { + push @{ $shape->[$j] }, @$contact_not_supported_by_capitals; + } + } + } +} + +sub clip_with_shape { + my ($self, $support, $shape) = @_; + + foreach my $i (keys %$support) { + // don't clip bottom layer with shape so that we + // can generate a continuous base flange + // also don't clip raft layers + next if $i == 0; + next if $i < $self->object_config->raft_layers; + $support->{$i} = intersection( + $support->{$i}, + $shape->[$i], + ); + } +} +*/ + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp new file mode 100644 index 000000000..4eabce772 --- /dev/null +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -0,0 +1,138 @@ +#ifndef slic3r_SupportCommon_hpp_ +#define slic3r_SupportCommon_hpp_ + +#include "../Polygon.hpp" +#include "SupportLayer.hpp" +#include "SupportParameters.hpp" + +namespace Slic3r { + +class PrintObject; +class SupportLayer; + +namespace FFFSupport { + +// Remove bridges from support contact areas. +// To be called if PrintObjectConfig::dont_support_bridges. +void remove_bridges_from_contacts( + const PrintConfig &print_config, + const Layer &lower_layer, + const LayerRegion &layerm, + float fw, + Polygons &contact_polygons); + +// Generate raft layers, also expand the 1st support layer +// in case there is no raft layer to improve support adhesion. +SupportGeneratorLayersPtr generate_raft_base( + const PrintObject &object, + const SupportParameters &support_params, + const SlicingParameters &slicing_params, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers, + const SupportGeneratorLayersPtr &base_layers, + SupportGeneratorLayerStorage &layer_storage); + +// returns sorted layers +SupportGeneratorLayersPtr generate_support_layers( + PrintObject &object, + const SupportGeneratorLayersPtr &raft_layers, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &intermediate_layers, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers); + +// Produce the support G-code. +// Used by both classic and tree supports. +void generate_support_toolpaths( + SupportLayerPtrs &support_layers, + const PrintObjectConfig &config, + const SupportParameters &support_params, + const SlicingParameters &slicing_params, + const SupportGeneratorLayersPtr &raft_layers, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + const SupportGeneratorLayersPtr &intermediate_layers, + const SupportGeneratorLayersPtr &interface_layers, + const SupportGeneratorLayersPtr &base_interface_layers); + +// FN_HIGHER_EQUAL: the provided object pointer has a Z value >= of an internal threshold. +// Find the first item with Z value >= of an internal threshold of fn_higher_equal. +// If no vec item with Z value >= of an internal threshold of fn_higher_equal is found, return vec.size() +// If the initial idx is size_t(-1), then use binary search. +// Otherwise search linearly upwards. +template +IndexType idx_higher_or_equal(IteratorType begin, IteratorType end, IndexType idx, FN_HIGHER_EQUAL fn_higher_equal) +{ + auto size = int(end - begin); + if (size == 0) { + idx = 0; + } else if (idx == IndexType(-1)) { + // First of the batch of layers per thread pool invocation. Use binary search. + int idx_low = 0; + int idx_high = std::max(0, size - 1); + while (idx_low + 1 < idx_high) { + int idx_mid = (idx_low + idx_high) / 2; + if (fn_higher_equal(begin[idx_mid])) + idx_high = idx_mid; + else + idx_low = idx_mid; + } + idx = fn_higher_equal(begin[idx_low]) ? idx_low : + (fn_higher_equal(begin[idx_high]) ? idx_high : size); + } else { + // For the other layers of this batch of layers, search incrementally, which is cheaper than the binary search. + while (int(idx) < size && ! fn_higher_equal(begin[idx])) + ++ idx; + } + return idx; +} +template +IndexType idx_higher_or_equal(const std::vector& vec, IndexType idx, FN_HIGHER_EQUAL fn_higher_equal) +{ + return idx_higher_or_equal(vec.begin(), vec.end(), idx, fn_higher_equal); +} + +// FN_LOWER_EQUAL: the provided object pointer has a Z value <= of an internal threshold. +// Find the first item with Z value <= of an internal threshold of fn_lower_equal. +// If no vec item with Z value <= of an internal threshold of fn_lower_equal is found, return -1. +// If the initial idx is < -1, then use binary search. +// Otherwise search linearly downwards. +template +int idx_lower_or_equal(IT begin, IT end, int idx, FN_LOWER_EQUAL fn_lower_equal) +{ + auto size = int(end - begin); + if (size == 0) { + idx = -1; + } else if (idx < -1) { + // First of the batch of layers per thread pool invocation. Use binary search. + int idx_low = 0; + int idx_high = std::max(0, size - 1); + while (idx_low + 1 < idx_high) { + int idx_mid = (idx_low + idx_high) / 2; + if (fn_lower_equal(begin[idx_mid])) + idx_low = idx_mid; + else + idx_high = idx_mid; + } + idx = fn_lower_equal(begin[idx_high]) ? idx_high : + (fn_lower_equal(begin[idx_low ]) ? idx_low : -1); + } else { + // For the other layers of this batch of layers, search incrementally, which is cheaper than the binary search. + while (idx >= 0 && ! fn_lower_equal(begin[idx])) + -- idx; + } + return idx; +} +template +int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lower_equal) +{ + return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); +} + +} // namespace FFFSupport + +} // namespace Slic3r + +#endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportDebug.cpp b/src/libslic3r/Support/SupportDebug.cpp new file mode 100644 index 000000000..8cec806c1 --- /dev/null +++ b/src/libslic3r/Support/SupportDebug.cpp @@ -0,0 +1,108 @@ +#if 1 //#ifdef SLIC3R_DEBUG + +#include "ClipperUtils.hpp" +#include "SVG.hpp" + +#include "../Layer.hpp" +#include "SupportLayer.hpp" + +namespace Slic3r::FFFSupport { + +const char* support_surface_type_to_color_name(const SupporLayerType surface_type) +{ + switch (surface_type) { + case SupporLayerType::TopContact: return "rgb(255,0,0)"; // "red"; + case SupporLayerType::TopInterface: return "rgb(0,255,0)"; // "green"; + case SupporLayerType::Base: return "rgb(0,0,255)"; // "blue"; + case SupporLayerType::BottomInterface:return "rgb(255,255,128)"; // yellow + case SupporLayerType::BottomContact: return "rgb(255,0,255)"; // magenta + case SupporLayerType::RaftInterface: return "rgb(0,255,255)"; + case SupporLayerType::RaftBase: return "rgb(128,128,128)"; + case SupporLayerType::Unknown: return "rgb(128,0,0)"; // maroon + default: return "rgb(64,64,64)"; + }; +} + +Point export_support_surface_type_legend_to_svg_box_size() +{ + return Point(scale_(1.+10.*8.), scale_(3.)); +} + +void export_support_surface_type_legend_to_svg(SVG &svg, const Point &pos) +{ + // 1st row + coord_t pos_x0 = pos(0) + scale_(1.); + coord_t pos_x = pos_x0; + coord_t pos_y = pos(1) + scale_(1.5); + coord_t step_x = scale_(10.); + svg.draw_legend(Point(pos_x, pos_y), "top contact" , support_surface_type_to_color_name(SupporLayerType::TopContact)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "top iface" , support_surface_type_to_color_name(SupporLayerType::TopInterface)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "base" , support_surface_type_to_color_name(SupporLayerType::Base)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "bottom iface" , support_surface_type_to_color_name(SupporLayerType::BottomInterface)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "bottom contact" , support_surface_type_to_color_name(SupporLayerType::BottomContact)); + // 2nd row + pos_x = pos_x0; + pos_y = pos(1)+scale_(2.8); + svg.draw_legend(Point(pos_x, pos_y), "raft interface" , support_surface_type_to_color_name(SupporLayerType::RaftInterface)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "raft base" , support_surface_type_to_color_name(SupporLayerType::RaftBase)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "unknown" , support_surface_type_to_color_name(SupporLayerType::Unknown)); + pos_x += step_x; + svg.draw_legend(Point(pos_x, pos_y), "intermediate" , support_surface_type_to_color_name(SupporLayerType::Intermediate)); +} + +void export_print_z_polygons_to_svg(const char *path, SupportGeneratorLayer ** const layers, int n_layers) +{ + BoundingBox bbox; + for (int i = 0; i < n_layers; ++ i) + bbox.merge(get_extents(layers[i]->polygons)); + Point legend_size = export_support_surface_type_legend_to_svg_box_size(); + Point legend_pos(bbox.min(0), bbox.max(1)); + bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1))); + SVG svg(path, bbox); + const float transparency = 0.5f; + for (int i = 0; i < n_layers; ++ i) + svg.draw(union_ex(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type), transparency); + for (int i = 0; i < n_layers; ++ i) + svg.draw(to_polylines(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type)); + export_support_surface_type_legend_to_svg(svg, legend_pos); + svg.Close(); +} + +void export_print_z_polygons_and_extrusions_to_svg( + const char *path, + SupportGeneratorLayer ** const layers, + int n_layers, + SupportLayer &support_layer) +{ + BoundingBox bbox; + for (int i = 0; i < n_layers; ++ i) + bbox.merge(get_extents(layers[i]->polygons)); + Point legend_size = export_support_surface_type_legend_to_svg_box_size(); + Point legend_pos(bbox.min(0), bbox.max(1)); + bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1))); + SVG svg(path, bbox); + const float transparency = 0.5f; + for (int i = 0; i < n_layers; ++ i) + svg.draw(union_ex(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type), transparency); + for (int i = 0; i < n_layers; ++ i) + svg.draw(to_polylines(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type)); + + Polygons polygons_support, polygons_interface; + support_layer.support_fills.polygons_covered_by_width(polygons_support, float(SCALED_EPSILON)); +// support_layer.support_interface_fills.polygons_covered_by_width(polygons_interface, SCALED_EPSILON); + svg.draw(union_ex(polygons_support), "brown"); + svg.draw(union_ex(polygons_interface), "black"); + + export_support_surface_type_legend_to_svg(svg, legend_pos); + svg.Close(); +} + +} // namespace Slic3r + +#endif /* SLIC3R_DEBUG */ diff --git a/src/libslic3r/Support/SupportDebug.hpp b/src/libslic3r/Support/SupportDebug.hpp new file mode 100644 index 000000000..22a43bc4e --- /dev/null +++ b/src/libslic3r/Support/SupportDebug.hpp @@ -0,0 +1,18 @@ +#ifndef slic3r_SupportCommon_hpp_ +#define slic3r_SupportCommon_hpp_ + +namespace Slic3r { + +class SupportGeneratorLayer; +class SupportLayer; + +namespace FFFSupport { + +void export_print_z_polygons_to_svg(const char *path, SupportGeneratorLayer ** const layers, size_t n_layers); +void export_print_z_polygons_and_extrusions_to_svg(const char *path, SupportGeneratorLayer ** const layers, size_t n_layers, SupportLayer& support_layer); + +} // namespace FFFSupport + +} // namespace Slic3r + +#endif /* slic3r_SupportCommon_hpp_ */ diff --git a/src/libslic3r/Support/SupportLayer.hpp b/src/libslic3r/Support/SupportLayer.hpp index 913e28136..82265881c 100644 --- a/src/libslic3r/Support/SupportLayer.hpp +++ b/src/libslic3r/Support/SupportLayer.hpp @@ -3,8 +3,10 @@ #include #include +// for Slic3r::deque +#include "../libslic3r.h" -namespace Slic3r { +namespace Slic3r::FFFSupport { // Support layer type to be used by SupportGeneratorLayer. This type carries a much more detailed information // about the support layer type than the final support layers stored in a PrintObject. @@ -111,18 +113,30 @@ public: // Layers are allocated and owned by a deque. Once a layer is allocated, it is maintained // up to the end of a generate() method. The layer storage may be replaced by an allocator class in the future, // which would allocate layers by multiple chunks. -#if 0 class SupportGeneratorLayerStorage { public: + SupportGeneratorLayer& allocate_unguarded(SupporLayerType layer_type) { + m_storage.emplace_back(); + m_storage.back().layer_type = layer_type; + return m_storage.back(); + } + + SupportGeneratorLayer& allocate(SupporLayerType layer_type) + { + m_mutex.lock(); + m_storage.emplace_back(); + SupportGeneratorLayer *layer_new = &m_storage.back(); + m_mutex.unlock(); + layer_new->layer_type = layer_type; + return *layer_new; + } + private: template using Allocator = tbb::scalable_allocator; - Slic3r::deque> m_data; + Slic3r::deque> m_storage; tbb::spin_mutex m_mutex; }; -#else -#endif -using SupportGeneratorLayerStorage = std::deque; using SupportGeneratorLayersPtr = std::vector; } // namespace Slic3r diff --git a/src/libslic3r/Support/SupportParameters.cpp b/src/libslic3r/Support/SupportParameters.cpp new file mode 100644 index 000000000..1c7f860b8 --- /dev/null +++ b/src/libslic3r/Support/SupportParameters.cpp @@ -0,0 +1,116 @@ +#include "../Print.hpp" +#include "../PrintConfig.hpp" +#include "../Slicing.hpp" +#include "SupportParameters.hpp" + +namespace Slic3r::FFFSupport { + +SupportParameters::SupportParameters(const PrintObject &object) +{ + const PrintConfig &print_config = object.print()->config(); + const PrintObjectConfig &object_config = object.config(); + const SlicingParameters &slicing_params = object.slicing_parameters(); + + this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height)); + this->support_material_flow = Slic3r::support_material_flow(&object, float(slicing_params.layer_height)); + this->support_material_interface_flow = Slic3r::support_material_interface_flow(&object, float(slicing_params.layer_height)); + this->raft_interface_flow = support_material_interface_flow; + + // Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um. + this->support_layer_height_min = scaled(0.01); + for (auto lh : print_config.min_layer_height.values) + this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, lh)); + for (auto layer : object.layers()) + this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height)); + + if (object_config.support_material_interface_layers.value == 0) { + // No interface layers allowed, print everything with the base support pattern. + this->support_material_interface_flow = this->support_material_flow; + } + + // Evaluate the XY gap between the object outer perimeters and the support structures. + // Evaluate the XY gap between the object outer perimeters and the support structures. + coordf_t external_perimeter_width = 0.; + coordf_t bridge_flow_ratio = 0; + for (size_t region_id = 0; region_id < object.num_printing_regions(); ++ region_id) { + const PrintRegion ®ion = object.printing_region(region_id); + external_perimeter_width = std::max(external_perimeter_width, coordf_t(region.flow(object, frExternalPerimeter, slicing_params.layer_height).width())); + bridge_flow_ratio += region.config().bridge_flow_ratio; + } + this->gap_xy = object_config.support_material_xy_spacing.get_abs_value(external_perimeter_width); + bridge_flow_ratio /= object.num_printing_regions(); + + this->support_material_bottom_interface_flow = slicing_params.soluble_interface || ! object_config.thick_bridges ? + this->support_material_interface_flow.with_flow_ratio(bridge_flow_ratio) : + Flow::bridging_flow(bridge_flow_ratio * this->support_material_interface_flow.nozzle_diameter(), this->support_material_interface_flow.nozzle_diameter()); + + this->can_merge_support_regions = object_config.support_material_extruder.value == object_config.support_material_interface_extruder.value; + if (!this->can_merge_support_regions && (object_config.support_material_extruder.value == 0 || object_config.support_material_interface_extruder.value == 0)) { + // One of the support extruders is of "don't care" type. + auto object_extruders = object.object_extruders(); + if (object_extruders.size() == 1 && + *object_extruders.begin() == std::max(object_config.support_material_extruder.value, object_config.support_material_interface_extruder.value)) + // Object is printed with the same extruder as the support. + this->can_merge_support_regions = true; + } + + + double interface_spacing = object_config.support_material_interface_spacing.value + this->support_material_interface_flow.spacing(); + this->interface_density = std::min(1., this->support_material_interface_flow.spacing() / interface_spacing); + double raft_interface_spacing = object_config.support_material_interface_spacing.value + this->raft_interface_flow.spacing(); + this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing); + double support_spacing = object_config.support_material_spacing.value + this->support_material_flow.spacing(); + this->support_density = std::min(1., this->support_material_flow.spacing() / support_spacing); + if (object_config.support_material_interface_layers.value == 0) { + // No interface layers allowed, print everything with the base support pattern. + this->interface_density = this->support_density; + } + + SupportMaterialPattern support_pattern = object_config.support_material_pattern; + this->with_sheath = object_config.support_material_with_sheath; + this->base_fill_pattern = + support_pattern == smpHoneycomb ? ipHoneycomb : + this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase; + this->interface_fill_pattern = (this->interface_density > 0.95 ? ipRectilinear : ipSupportBase); + this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase; + this->contact_fill_pattern = + (object_config.support_material_interface_pattern == smipAuto && slicing_params.soluble_interface) || + object_config.support_material_interface_pattern == smipConcentric ? + ipConcentric : + (this->interface_density > 0.95 ? ipRectilinear : ipSupportBase); + + this->base_angle = Geometry::deg2rad(float(object_config.support_material_angle.value)); + this->interface_angle = Geometry::deg2rad(float(object_config.support_material_angle.value + 90.)); + this->raft_angle_1st_layer = 0.f; + this->raft_angle_base = 0.f; + this->raft_angle_interface = 0.f; + if (slicing_params.base_raft_layers > 1) { + assert(slicing_params.raft_layers() >= 4); + // There are all raft layer types (1st layer, base, interface & contact layers) available. + this->raft_angle_1st_layer = this->interface_angle; + this->raft_angle_base = this->base_angle; + this->raft_angle_interface = this->interface_angle; + if ((slicing_params.interface_raft_layers & 1) == 0) + // Allign the 1st raft interface layer so that the object 1st layer is hatched perpendicularly to the raft contact interface. + this->raft_angle_interface += float(0.5 * M_PI); + } else if (slicing_params.base_raft_layers == 1 || slicing_params.interface_raft_layers > 1) { + assert(slicing_params.raft_layers() == 2 || slicing_params.raft_layers() == 3); + // 1st layer, interface & contact layers available. + this->raft_angle_1st_layer = this->base_angle; + this->raft_angle_interface = this->interface_angle + 0.5 * M_PI; + } else if (slicing_params.interface_raft_layers == 1) { + // Only the contact raft layer is non-empty, which will be printed as the 1st layer. + assert(slicing_params.base_raft_layers == 0); + assert(slicing_params.interface_raft_layers == 1); + assert(slicing_params.raft_layers() == 1); + this->raft_angle_1st_layer = float(0.5 * M_PI); + this->raft_angle_interface = this->raft_angle_1st_layer; + } else { + // No raft. + assert(slicing_params.base_raft_layers == 0); + assert(slicing_params.interface_raft_layers == 0); + assert(slicing_params.raft_layers() == 0); + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp index fd4f1f8b7..904e8ffe2 100644 --- a/src/libslic3r/Support/SupportParameters.hpp +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -9,6 +9,8 @@ namespace Slic3r { class PrintObject; enum InfillPattern : int; +namespace FFFSupport { + struct SupportParameters { SupportParameters(const PrintObject &object); @@ -61,6 +63,8 @@ struct SupportParameters { { return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); } }; +} // namespace FFFSupport + } // namespace Slic3r #endif /* slic3r_SupportParameters_hpp_ */ diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/SupportMaterial.cpp index 224216466..40221ec2a 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/SupportMaterial.cpp @@ -8,6 +8,8 @@ #include "Point.hpp" #include "MutablePolygon.hpp" +#include "Support/SupportCommon.hpp" + #include #include @@ -16,7 +18,6 @@ #include #include -#include #include #define SUPPORT_USE_AGG_RASTERIZER @@ -43,12 +44,10 @@ #include "SVG.hpp" #endif -#pragma message ("TODO: Wrap svg usages in DEBUG ifdef and remove the following include") -#include "SVG.hpp" - -// #undef NDEBUG #include +using namespace Slic3r::FFFSupport; + namespace Slic3r { // how much we extend support around the actual contact area @@ -66,103 +65,6 @@ namespace Slic3r { //#define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtMiter, 1.5 #define SUPPORT_SURFACES_OFFSET_PARAMETERS ClipperLib::jtSquare, 0. -#if 1 //#ifdef SLIC3R_DEBUG -const char* support_surface_type_to_color_name(const SupporLayerType surface_type) -{ - switch (surface_type) { - case SupporLayerType::TopContact: return "rgb(255,0,0)"; // "red"; - case SupporLayerType::TopInterface: return "rgb(0,255,0)"; // "green"; - case SupporLayerType::Base: return "rgb(0,0,255)"; // "blue"; - case SupporLayerType::BottomInterface:return "rgb(255,255,128)"; // yellow - case SupporLayerType::BottomContact: return "rgb(255,0,255)"; // magenta - case SupporLayerType::RaftInterface: return "rgb(0,255,255)"; - case SupporLayerType::RaftBase: return "rgb(128,128,128)"; - case SupporLayerType::Unknown: return "rgb(128,0,0)"; // maroon - default: return "rgb(64,64,64)"; - }; -} - -Point export_support_surface_type_legend_to_svg_box_size() -{ - return Point(scale_(1.+10.*8.), scale_(3.)); -} - -void export_support_surface_type_legend_to_svg(SVG &svg, const Point &pos) -{ - // 1st row - coord_t pos_x0 = pos(0) + scale_(1.); - coord_t pos_x = pos_x0; - coord_t pos_y = pos(1) + scale_(1.5); - coord_t step_x = scale_(10.); - svg.draw_legend(Point(pos_x, pos_y), "top contact" , support_surface_type_to_color_name(SupporLayerType::TopContact)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "top iface" , support_surface_type_to_color_name(SupporLayerType::TopInterface)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "base" , support_surface_type_to_color_name(SupporLayerType::Base)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "bottom iface" , support_surface_type_to_color_name(SupporLayerType::BottomInterface)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "bottom contact" , support_surface_type_to_color_name(SupporLayerType::BottomContact)); - // 2nd row - pos_x = pos_x0; - pos_y = pos(1)+scale_(2.8); - svg.draw_legend(Point(pos_x, pos_y), "raft interface" , support_surface_type_to_color_name(SupporLayerType::RaftInterface)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "raft base" , support_surface_type_to_color_name(SupporLayerType::RaftBase)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "unknown" , support_surface_type_to_color_name(SupporLayerType::Unknown)); - pos_x += step_x; - svg.draw_legend(Point(pos_x, pos_y), "intermediate" , support_surface_type_to_color_name(SupporLayerType::Intermediate)); -} - -void export_print_z_polygons_to_svg(const char *path, SupportGeneratorLayer ** const layers, int n_layers) -{ - BoundingBox bbox; - for (int i = 0; i < n_layers; ++ i) - bbox.merge(get_extents(layers[i]->polygons)); - Point legend_size = export_support_surface_type_legend_to_svg_box_size(); - Point legend_pos(bbox.min(0), bbox.max(1)); - bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1))); - SVG svg(path, bbox); - const float transparency = 0.5f; - for (int i = 0; i < n_layers; ++ i) - svg.draw(union_ex(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type), transparency); - for (int i = 0; i < n_layers; ++ i) - svg.draw(to_polylines(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type)); - export_support_surface_type_legend_to_svg(svg, legend_pos); - svg.Close(); -} - -void export_print_z_polygons_and_extrusions_to_svg( - const char *path, - SupportGeneratorLayer ** const layers, - int n_layers, - SupportLayer &support_layer) -{ - BoundingBox bbox; - for (int i = 0; i < n_layers; ++ i) - bbox.merge(get_extents(layers[i]->polygons)); - Point legend_size = export_support_surface_type_legend_to_svg_box_size(); - Point legend_pos(bbox.min(0), bbox.max(1)); - bbox.merge(Point(std::max(bbox.min(0) + legend_size(0), bbox.max(0)), bbox.max(1) + legend_size(1))); - SVG svg(path, bbox); - const float transparency = 0.5f; - for (int i = 0; i < n_layers; ++ i) - svg.draw(union_ex(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type), transparency); - for (int i = 0; i < n_layers; ++ i) - svg.draw(to_polylines(layers[i]->polygons), support_surface_type_to_color_name(layers[i]->layer_type)); - - Polygons polygons_support, polygons_interface; - support_layer.support_fills.polygons_covered_by_width(polygons_support, float(SCALED_EPSILON)); -// support_layer.support_interface_fills.polygons_covered_by_width(polygons_interface, SCALED_EPSILON); - svg.draw(union_ex(polygons_support), "brown"); - svg.draw(union_ex(polygons_interface), "black"); - - export_support_surface_type_legend_to_svg(svg, legend_pos); - svg.Close(); -} -#endif /* SLIC3R_DEBUG */ - #ifdef SUPPORT_USE_AGG_RASTERIZER static std::vector rasterize_polygons(const Vec2i &grid_size, const double pixel_size, const Point &left_bottom, const Polygons &polygons) { @@ -326,114 +228,6 @@ static Polygons contours_simplified(const Vec2i &grid_size, const double pixel_s } #endif // SUPPORT_USE_AGG_RASTERIZER -SupportParameters::SupportParameters(const PrintObject &object) -{ - const PrintConfig &print_config = object.print()->config(); - const PrintObjectConfig &object_config = object.config(); - const SlicingParameters &slicing_params = object.slicing_parameters(); - - this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height)); - this->support_material_flow = Slic3r::support_material_flow(&object, float(slicing_params.layer_height)); - this->support_material_interface_flow = Slic3r::support_material_interface_flow(&object, float(slicing_params.layer_height)); - this->raft_interface_flow = support_material_interface_flow; - - // Calculate a minimum support layer height as a minimum over all extruders, but not smaller than 10um. - this->support_layer_height_min = scaled(0.01); - for (auto lh : print_config.min_layer_height.values) - this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, lh)); - for (auto layer : object.layers()) - this->support_layer_height_min = std::min(this->support_layer_height_min, std::max(0.01, layer->height)); - - if (object_config.support_material_interface_layers.value == 0) { - // No interface layers allowed, print everything with the base support pattern. - this->support_material_interface_flow = this->support_material_flow; - } - - // Evaluate the XY gap between the object outer perimeters and the support structures. - // Evaluate the XY gap between the object outer perimeters and the support structures. - coordf_t external_perimeter_width = 0.; - coordf_t bridge_flow_ratio = 0; - for (size_t region_id = 0; region_id < object.num_printing_regions(); ++ region_id) { - const PrintRegion ®ion = object.printing_region(region_id); - external_perimeter_width = std::max(external_perimeter_width, coordf_t(region.flow(object, frExternalPerimeter, slicing_params.layer_height).width())); - bridge_flow_ratio += region.config().bridge_flow_ratio; - } - this->gap_xy = object_config.support_material_xy_spacing.get_abs_value(external_perimeter_width); - bridge_flow_ratio /= object.num_printing_regions(); - - this->support_material_bottom_interface_flow = slicing_params.soluble_interface || ! object_config.thick_bridges ? - this->support_material_interface_flow.with_flow_ratio(bridge_flow_ratio) : - Flow::bridging_flow(bridge_flow_ratio * this->support_material_interface_flow.nozzle_diameter(), this->support_material_interface_flow.nozzle_diameter()); - - this->can_merge_support_regions = object_config.support_material_extruder.value == object_config.support_material_interface_extruder.value; - if (!this->can_merge_support_regions && (object_config.support_material_extruder.value == 0 || object_config.support_material_interface_extruder.value == 0)) { - // One of the support extruders is of "don't care" type. - auto object_extruders = object.object_extruders(); - if (object_extruders.size() == 1 && - *object_extruders.begin() == std::max(object_config.support_material_extruder.value, object_config.support_material_interface_extruder.value)) - // Object is printed with the same extruder as the support. - this->can_merge_support_regions = true; - } - - - double interface_spacing = object_config.support_material_interface_spacing.value + this->support_material_interface_flow.spacing(); - this->interface_density = std::min(1., this->support_material_interface_flow.spacing() / interface_spacing); - double raft_interface_spacing = object_config.support_material_interface_spacing.value + this->raft_interface_flow.spacing(); - this->raft_interface_density = std::min(1., this->raft_interface_flow.spacing() / raft_interface_spacing); - double support_spacing = object_config.support_material_spacing.value + this->support_material_flow.spacing(); - this->support_density = std::min(1., this->support_material_flow.spacing() / support_spacing); - if (object_config.support_material_interface_layers.value == 0) { - // No interface layers allowed, print everything with the base support pattern. - this->interface_density = this->support_density; - } - - SupportMaterialPattern support_pattern = object_config.support_material_pattern; - this->with_sheath = object_config.support_material_with_sheath; - this->base_fill_pattern = - support_pattern == smpHoneycomb ? ipHoneycomb : - this->support_density > 0.95 || this->with_sheath ? ipRectilinear : ipSupportBase; - this->interface_fill_pattern = (this->interface_density > 0.95 ? ipRectilinear : ipSupportBase); - this->raft_interface_fill_pattern = this->raft_interface_density > 0.95 ? ipRectilinear : ipSupportBase; - this->contact_fill_pattern = - (object_config.support_material_interface_pattern == smipAuto && slicing_params.soluble_interface) || - object_config.support_material_interface_pattern == smipConcentric ? - ipConcentric : - (this->interface_density > 0.95 ? ipRectilinear : ipSupportBase); - - this->base_angle = Geometry::deg2rad(float(object_config.support_material_angle.value)); - this->interface_angle = Geometry::deg2rad(float(object_config.support_material_angle.value + 90.)); - this->raft_angle_1st_layer = 0.f; - this->raft_angle_base = 0.f; - this->raft_angle_interface = 0.f; - if (slicing_params.base_raft_layers > 1) { - assert(slicing_params.raft_layers() >= 4); - // There are all raft layer types (1st layer, base, interface & contact layers) available. - this->raft_angle_1st_layer = this->interface_angle; - this->raft_angle_base = this->base_angle; - this->raft_angle_interface = this->interface_angle; - if ((slicing_params.interface_raft_layers & 1) == 0) - // Allign the 1st raft interface layer so that the object 1st layer is hatched perpendicularly to the raft contact interface. - this->raft_angle_interface += float(0.5 * M_PI); - } else if (slicing_params.base_raft_layers == 1 || slicing_params.interface_raft_layers > 1) { - assert(slicing_params.raft_layers() == 2 || slicing_params.raft_layers() == 3); - // 1st layer, interface & contact layers available. - this->raft_angle_1st_layer = this->base_angle; - this->raft_angle_interface = this->interface_angle + 0.5 * M_PI; - } else if (slicing_params.interface_raft_layers == 1) { - // Only the contact raft layer is non-empty, which will be printed as the 1st layer. - assert(slicing_params.base_raft_layers == 0); - assert(slicing_params.interface_raft_layers == 1); - assert(slicing_params.raft_layers() == 1); - this->raft_angle_1st_layer = float(0.5 * M_PI); - this->raft_angle_interface = this->raft_angle_1st_layer; - } else { - // No raft. - assert(slicing_params.base_raft_layers == 0); - assert(slicing_params.interface_raft_layers == 0); - assert(slicing_params.raft_layers() == 0); - } -} - PrintObjectSupportMaterial::PrintObjectSupportMaterial(const PrintObject *object, const SlicingParameters &slicing_params) : m_print_config (&object->print()->config()), m_object_config (&object->config()), @@ -442,39 +236,6 @@ PrintObjectSupportMaterial::PrintObjectSupportMaterial(const PrintObject *object { } -// Using the std::deque as an allocator. -inline SupportGeneratorLayer& layer_allocate( - std::deque &layer_storage, - SupporLayerType layer_type) -{ - layer_storage.push_back(SupportGeneratorLayer()); - layer_storage.back().layer_type = layer_type; - return layer_storage.back(); -} - -inline SupportGeneratorLayer& layer_allocate( - std::deque &layer_storage, - tbb::spin_mutex &layer_storage_mutex, - SupporLayerType layer_type) -{ - layer_storage_mutex.lock(); - layer_storage.push_back(SupportGeneratorLayer()); - SupportGeneratorLayer *layer_new = &layer_storage.back(); - layer_storage_mutex.unlock(); - layer_new->layer_type = layer_type; - return *layer_new; -} - -inline void layers_append(SupportGeneratorLayersPtr &dst, const SupportGeneratorLayersPtr &src) -{ - dst.insert(dst.end(), src.begin(), src.end()); -} - -// Support layer that is covered by some form of dense interface. -static constexpr const std::initializer_list support_types_interface { - SupporLayerType::RaftInterface, SupporLayerType::BottomContact, SupporLayerType::BottomInterface, SupporLayerType::TopContact, SupporLayerType::TopInterface -}; - void PrintObjectSupportMaterial::generate(PrintObject &object) { BOOST_LOG_TRIVIAL(info) << "Support generator - Start"; @@ -1296,86 +1057,6 @@ namespace SupportMaterialInternal { } } -void remove_bridges_from_contacts( - const PrintConfig &print_config, - const Layer &lower_layer, - const LayerRegion &layerm, - float fw, - Polygons &contact_polygons) -{ - // compute the area of bridging perimeters - Polygons bridges; - { - // Surface supporting this layer, expanded by 0.5 * nozzle_diameter, as we consider this kind of overhang to be sufficiently supported. - Polygons lower_grown_slices = expand(lower_layer.lslices, - //FIXME to mimic the decision in the perimeter generator, we should use half the external perimeter width. - 0.5f * float(scale_(print_config.nozzle_diameter.get_at(layerm.region().config().perimeter_extruder-1))), - SUPPORT_SURFACES_OFFSET_PARAMETERS); - // Collect perimeters of this layer. - //FIXME split_at_first_point() could split a bridge mid-way - #if 0 - Polylines overhang_perimeters = layerm.perimeters.as_polylines(); - // workaround for Clipper bug, see Slic3r::Polygon::clip_as_polyline() - for (Polyline &polyline : overhang_perimeters) - polyline.points[0].x += 1; - // Trim the perimeters of this layer by the lower layer to get the unsupported pieces of perimeters. - overhang_perimeters = diff_pl(overhang_perimeters, lower_grown_slices); - #else - Polylines overhang_perimeters = diff_pl(layerm.perimeters().as_polylines(), lower_grown_slices); - #endif - - // only consider straight overhangs - // only consider overhangs having endpoints inside layer's slices - // convert bridging polylines into polygons by inflating them with their thickness - // since we're dealing with bridges, we can't assume width is larger than spacing, - // so we take the largest value and also apply safety offset to be ensure no gaps - // are left in between - Flow perimeter_bridge_flow = layerm.bridging_flow(frPerimeter); - //FIXME one may want to use a maximum of bridging flow width and normal flow width, as the perimeters are calculated using the normal flow - // and then turned to bridging flow, thus their centerlines are derived from non-bridging flow and expanding them by a bridging flow - // may not expand them to the edge of their respective islands. - const float w = float(0.5 * std::max(perimeter_bridge_flow.scaled_width(), perimeter_bridge_flow.scaled_spacing())) + scaled(0.001); - for (Polyline &polyline : overhang_perimeters) - if (polyline.is_straight()) { - // This is a bridge - polyline.extend_start(fw); - polyline.extend_end(fw); - // Is the straight perimeter segment supported at both sides? - Point pts[2] = { polyline.first_point(), polyline.last_point() }; - bool supported[2] = { false, false }; - for (size_t i = 0; i < lower_layer.lslices.size() && ! (supported[0] && supported[1]); ++ i) - for (int j = 0; j < 2; ++ j) - if (! supported[j] && lower_layer.lslices_ex[i].bbox.contains(pts[j]) && lower_layer.lslices[i].contains(pts[j])) - supported[j] = true; - if (supported[0] && supported[1]) - // Offset a polyline into a thick line. - polygons_append(bridges, offset(polyline, w)); - } - bridges = union_(bridges); - } - // remove the entire bridges and only support the unsupported edges - //FIXME the brided regions are already collected as layerm.bridged. Use it? - for (const Surface &surface : layerm.fill_surfaces()) - if (surface.surface_type == stBottomBridge && surface.bridge_angle >= 0.0) - polygons_append(bridges, surface.expolygon); - //FIXME add the gap filled areas. Extrude the gaps with a bridge flow? - // Remove the unsupported ends of the bridges from the bridged areas. - //FIXME add supports at regular intervals to support long bridges! - bridges = diff(bridges, - // Offset unsupported edges into polygons. - offset(layerm.unsupported_bridge_edges(), scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS)); - // Remove bridged areas from the supported areas. - contact_polygons = diff(contact_polygons, bridges, ApplySafetyOffset::Yes); - - #ifdef SLIC3R_DEBUG - static int iRun = 0; - SVG::export_expolygons(debug_out_path("support-top-contacts-remove-bridges-run%d.svg", iRun ++), - { { { union_ex(offset(layerm.unsupported_bridge_edges(), scale_(SUPPORT_MATERIAL_MARGIN), SUPPORT_SURFACES_OFFSET_PARAMETERS)) }, { "unsupported_bridge_edges", "orange", 0.5f } }, - { { union_ex(contact_polygons) }, { "contact_polygons", "blue", 0.5f } }, - { { union_ex(bridges) }, { "bridges", "red", "black", "", scaled(0.1f), 0.5f } } }); - #endif /* SLIC3R_DEBUG */ -} - std::vector PrintObjectSupportMaterial::buildplate_covered(const PrintObject &object) const { // Build support on a build plate only? If so, then collect and union all the surfaces below the current layer. @@ -1674,8 +1355,7 @@ static inline std::pair new_cont const SlicingParameters &slicing_params, const coordf_t support_layer_height_min, const Layer &layer, - std::deque &layer_storage, - tbb::spin_mutex &layer_storage_mutex) + SupportGeneratorLayerStorage &layer_storage) { double print_z, bottom_z, height; SupportGeneratorLayer* bridging_layer = nullptr; @@ -1735,7 +1415,7 @@ static inline std::pair new_cont } if (bridging_print_z < print_z - EPSILON) { // Allocate the new layer. - bridging_layer = &layer_allocate(layer_storage, layer_storage_mutex, SupporLayerType::TopContact); + bridging_layer = &layer_storage.allocate(SupporLayerType::TopContact); bridging_layer->idx_object_layer_above = layer_id; bridging_layer->print_z = bridging_print_z; if (bridging_print_z == slicing_params.first_print_layer_height) { @@ -1751,7 +1431,7 @@ static inline std::pair new_cont } } - SupportGeneratorLayer &new_layer = layer_allocate(layer_storage, layer_storage_mutex, SupporLayerType::TopContact); + SupportGeneratorLayer &new_layer = layer_storage.allocate(SupporLayerType::TopContact); new_layer.idx_object_layer_above = layer_id; new_layer.print_z = print_z; new_layer.bottom_z = bottom_z; @@ -1983,9 +1663,8 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::top_contact_layers( // For each overhang layer, two supporting layers may be generated: One for the overhangs extruded with a bridging flow, // and the other for the overhangs extruded with a normal flow. contact_out.assign(num_layers * 2, nullptr); - tbb::spin_mutex layer_storage_mutex; tbb::parallel_for(tbb::blocked_range(this->has_raft() ? 0 : 1, num_layers), - [this, &object, &annotations, &layer_storage, &layer_storage_mutex, &contact_out] + [this, &object, &annotations, &layer_storage, &contact_out] (const tbb::blocked_range& range) { for (size_t layer_id = range.begin(); layer_id < range.end(); ++ layer_id) { @@ -2003,7 +1682,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::top_contact_layers( // Now apply the contact areas to the layer where they need to be made. if (! contact_polygons.empty() || ! overhang_polygons.empty()) { // Allocate the two empty layers. - auto [new_layer, bridging_layer] = new_contact_layer(*m_print_config, *m_object_config, m_slicing_params, m_support_params.support_layer_height_min, layer, layer_storage, layer_storage_mutex); + auto [new_layer, bridging_layer] = new_contact_layer(*m_print_config, *m_object_config, m_slicing_params, m_support_params.support_layer_height_min, layer, layer_storage); if (new_layer) { // Fill the non-bridging layer with polygons. fill_contact_layer(*new_layer, layer_id, m_slicing_params, @@ -2053,7 +1732,7 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( // First top contact layer index overlapping with this new bottom interface layer. size_t contact_idx, // To allocate a new layer from. - std::deque &layer_storage, + SupportGeneratorLayerStorage &layer_storage, // To trim the support areas above this bottom interface layer with this newly created bottom interface layer. std::vector &layer_support_areas, // Support areas projected from top to bottom, starting with top support interfaces. @@ -2088,7 +1767,7 @@ static inline SupportGeneratorLayer* detect_bottom_contacts( size_t layer_id = layer.id() - slicing_params.raft_layers(); // Allocate a new bottom contact layer. - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::BottomContact); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::BottomContact); // Grow top surfaces so that interface and support generation are generated // with some spacing from object - it looks we don't need the actual // top shapes so this can be done here @@ -2392,80 +2071,6 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_ return bottom_contacts; } -// FN_HIGHER_EQUAL: the provided object pointer has a Z value >= of an internal threshold. -// Find the first item with Z value >= of an internal threshold of fn_higher_equal. -// If no vec item with Z value >= of an internal threshold of fn_higher_equal is found, return vec.size() -// If the initial idx is size_t(-1), then use binary search. -// Otherwise search linearly upwards. -template -IndexType idx_higher_or_equal(IteratorType begin, IteratorType end, IndexType idx, FN_HIGHER_EQUAL fn_higher_equal) -{ - auto size = int(end - begin); - if (size == 0) { - idx = 0; - } else if (idx == IndexType(-1)) { - // First of the batch of layers per thread pool invocation. Use binary search. - int idx_low = 0; - int idx_high = std::max(0, size - 1); - while (idx_low + 1 < idx_high) { - int idx_mid = (idx_low + idx_high) / 2; - if (fn_higher_equal(begin[idx_mid])) - idx_high = idx_mid; - else - idx_low = idx_mid; - } - idx = fn_higher_equal(begin[idx_low]) ? idx_low : - (fn_higher_equal(begin[idx_high]) ? idx_high : size); - } else { - // For the other layers of this batch of layers, search incrementally, which is cheaper than the binary search. - while (int(idx) < size && ! fn_higher_equal(begin[idx])) - ++ idx; - } - return idx; -} -template -IndexType idx_higher_or_equal(const std::vector& vec, IndexType idx, FN_HIGHER_EQUAL fn_higher_equal) -{ - return idx_higher_or_equal(vec.begin(), vec.end(), idx, fn_higher_equal); -} - -// FN_LOWER_EQUAL: the provided object pointer has a Z value <= of an internal threshold. -// Find the first item with Z value <= of an internal threshold of fn_lower_equal. -// If no vec item with Z value <= of an internal threshold of fn_lower_equal is found, return -1. -// If the initial idx is < -1, then use binary search. -// Otherwise search linearly downwards. -template -int idx_lower_or_equal(IT begin, IT end, int idx, FN_LOWER_EQUAL fn_lower_equal) -{ - auto size = int(end - begin); - if (size == 0) { - idx = -1; - } else if (idx < -1) { - // First of the batch of layers per thread pool invocation. Use binary search. - int idx_low = 0; - int idx_high = std::max(0, size - 1); - while (idx_low + 1 < idx_high) { - int idx_mid = (idx_low + idx_high) / 2; - if (fn_lower_equal(begin[idx_mid])) - idx_low = idx_mid; - else - idx_high = idx_mid; - } - idx = fn_lower_equal(begin[idx_high]) ? idx_high : - (fn_lower_equal(begin[idx_low ]) ? idx_low : -1); - } else { - // For the other layers of this batch of layers, search incrementally, which is cheaper than the binary search. - while (idx >= 0 && ! fn_lower_equal(begin[idx])) - -- idx; - } - return idx; -} -template -int idx_lower_or_equal(const std::vector &vec, int idx, FN_LOWER_EQUAL fn_lower_equal) -{ - return idx_lower_or_equal(vec.begin(), vec.end(), idx, fn_lower_equal); -} - // Trim the top_contacts layers with the bottom_contacts layers if they overlap, so there would not be enough vertical space for both of them. void PrintObjectSupportMaterial::trim_top_contacts_by_bottom_contacts( const PrintObject &object, const SupportGeneratorLayersPtr &bottom_contacts, SupportGeneratorLayersPtr &top_contacts) const @@ -2561,7 +2166,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp assert(extr2->bottom_z == m_slicing_params.first_print_layer_height); assert(extr2->print_z >= m_slicing_params.first_print_layer_height + m_support_params.support_layer_height_min - EPSILON); if (intermediate_layers.empty() || intermediate_layers.back()->print_z < m_slicing_params.first_print_layer_height) { - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); layer_new.bottom_z = 0.; layer_new.print_z = m_slicing_params.first_print_layer_height; layer_new.height = m_slicing_params.first_print_layer_height; @@ -2583,7 +2188,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp // At this point only layers above first_print_layer_heigth + EPSILON are expected as the other cases were captured earlier. assert(extr2z >= m_slicing_params.first_print_layer_height + EPSILON); // Generate a new intermediate layer. - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); layer_new.bottom_z = 0.; layer_new.print_z = extr1z = m_slicing_params.first_print_layer_height; layer_new.height = extr1z; @@ -2603,7 +2208,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp ++ idx_layer_object; if (idx_layer_object == 0 && extr1z == m_slicing_params.raft_interface_top_z) { // Insert one base support layer below the object. - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); layer_new.print_z = m_slicing_params.object_print_z_min; layer_new.bottom_z = m_slicing_params.raft_interface_top_z; layer_new.height = layer_new.print_z - layer_new.bottom_z; @@ -2611,7 +2216,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp } // Emit all intermediate support layers synchronized with object layers up to extr2z. for (; idx_layer_object < object.layers().size() && object.layers()[idx_layer_object]->print_z < extr2z + EPSILON; ++ idx_layer_object) { - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); layer_new.print_z = object.layers()[idx_layer_object]->print_z; layer_new.height = object.layers()[idx_layer_object]->height; layer_new.bottom_z = (idx_layer_object > 0) ? object.layers()[idx_layer_object - 1]->print_z : (layer_new.print_z - layer_new.height); @@ -2629,7 +2234,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp // between the 1st intermediate layer print_z and extr1->print_z is not too small. assert(extr1->bottom_z + m_support_params.support_layer_height_min < extr1->print_z + EPSILON); // Generate the first intermediate layer. - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); layer_new.bottom_z = extr1->bottom_z; layer_new.print_z = extr1z = extr1->print_z; layer_new.height = extr1->height; @@ -2653,7 +2258,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::raft_and_intermediate_supp coordf_t extr2z_large_steps = extr2z; // Take the largest allowed step in the Z axis until extr2z_large_steps is reached. for (size_t i = 0; i < n_layers_extra; ++ i) { - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, SupporLayerType::Intermediate); + SupportGeneratorLayer &layer_new = layer_storage.allocate_unguarded(SupporLayerType::Intermediate); if (i + 1 == n_layers_extra) { // Last intermediate layer added. Align the last entered layer with extr2z_large_steps exactly. layer_new.bottom_z = (i == 0) ? extr1z : intermediate_layers.back()->print_z; @@ -2909,163 +2514,6 @@ void PrintObjectSupportMaterial::trim_support_layers_by_object( BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::trim_support_layers_by_object() in parallel - end"; } -SupportGeneratorLayersPtr generate_raft_base( - const PrintObject &object, - const SupportParameters &support_params, - const SlicingParameters &slicing_params, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers, - const SupportGeneratorLayersPtr &base_layers, - SupportGeneratorLayerStorage &layer_storage) -{ - // If there is brim to be generated, calculate the trimming regions. - Polygons brim; - if (object.has_brim()) { - // The object does not have a raft. - // Calculate the area covered by the brim. - const BrimType brim_type = object.config().brim_type; - const bool brim_outer = brim_type == btOuterOnly || brim_type == btOuterAndInner; - const bool brim_inner = brim_type == btInnerOnly || brim_type == btOuterAndInner; - const auto brim_separation = scaled(object.config().brim_separation.value + object.config().brim_width.value); - for (const ExPolygon &ex : object.layers().front()->lslices) { - if (brim_outer && brim_inner) - polygons_append(brim, offset(ex, brim_separation)); - else { - if (brim_outer) - polygons_append(brim, offset(ex.contour, brim_separation, ClipperLib::jtRound, float(scale_(0.1)))); - else - brim.emplace_back(ex.contour); - if (brim_inner) { - Polygons holes = ex.holes; - polygons_reverse(holes); - holes = shrink(holes, brim_separation, ClipperLib::jtRound, float(scale_(0.1))); - polygons_reverse(holes); - polygons_append(brim, std::move(holes)); - } else - polygons_append(brim, ex.holes); - } - } - brim = union_(brim); - } - - // How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed. - const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON)); - const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine); - SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front(); - SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front(); - SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front(); - SupportGeneratorLayer *columns_base = base_layers .empty() ? nullptr : base_layers .front(); - if (contacts != nullptr && contacts->print_z > std::max(slicing_params.first_print_layer_height, slicing_params.raft_contact_top_z) + EPSILON) - // This is not the raft contact layer. - contacts = nullptr; - if (interfaces != nullptr && interfaces->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) - // This is not the raft column base layer. - interfaces = nullptr; - if (base_interfaces != nullptr && base_interfaces->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) - // This is not the raft column base layer. - base_interfaces = nullptr; - if (columns_base != nullptr && columns_base->bottom_print_z() > slicing_params.raft_interface_top_z + EPSILON) - // This is not the raft interface layer. - columns_base = nullptr; - - Polygons interface_polygons; - if (contacts != nullptr && ! contacts->polygons.empty()) - polygons_append(interface_polygons, expand(contacts->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); - if (interfaces != nullptr && ! interfaces->polygons.empty()) - polygons_append(interface_polygons, expand(interfaces->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); - if (base_interfaces != nullptr && ! base_interfaces->polygons.empty()) - polygons_append(interface_polygons, expand(base_interfaces->polygons, inflate_factor_fine, SUPPORT_SURFACES_OFFSET_PARAMETERS)); - - // Output vector. - SupportGeneratorLayersPtr raft_layers; - - if (slicing_params.raft_layers() > 1) { - Polygons base; - Polygons columns; - Polygons first_layer; - if (columns_base != nullptr) { - if (columns_base->bottom_print_z() > slicing_params.raft_interface_top_z - EPSILON) { - // Classic supports with colums above the raft interface. - base = columns_base->polygons; - columns = base; - if (! interface_polygons.empty()) - // Trim the 1st layer columns with the inflated interface polygons. - columns = diff(columns, interface_polygons); - } else { - // Organic supports with raft on print bed. - assert(is_approx(columns_base->print_z, slicing_params.first_print_layer_height)); - first_layer = columns_base->polygons; - } - } - if (! interface_polygons.empty()) { - // Merge the untrimmed columns base with the expanded raft interface, to be used for the support base and interface. - base = union_(base, interface_polygons); - } - // Do not add the raft contact layer, only add the raft layers below the contact layer. - // Insert the 1st layer. - { - SupportGeneratorLayer &new_layer = layer_allocate(layer_storage, (slicing_params.base_raft_layers > 0) ? SupporLayerType::RaftBase : SupporLayerType::RaftInterface); - raft_layers.push_back(&new_layer); - new_layer.print_z = slicing_params.first_print_layer_height; - new_layer.height = slicing_params.first_print_layer_height; - new_layer.bottom_z = 0.; - first_layer = union_(std::move(first_layer), base); - new_layer.polygons = inflate_factor_1st_layer > 0 ? expand(first_layer, inflate_factor_1st_layer) : first_layer; - } - // Insert the base layers. - for (size_t i = 1; i < slicing_params.base_raft_layers; ++ i) { - coordf_t print_z = raft_layers.back()->print_z; - SupportGeneratorLayer &new_layer = layer_allocate(layer_storage, SupporLayerType::RaftBase); - raft_layers.push_back(&new_layer); - new_layer.print_z = print_z + slicing_params.base_raft_layer_height; - new_layer.height = slicing_params.base_raft_layer_height; - new_layer.bottom_z = print_z; - new_layer.polygons = base; - } - // Insert the interface layers. - for (size_t i = 1; i < slicing_params.interface_raft_layers; ++ i) { - coordf_t print_z = raft_layers.back()->print_z; - SupportGeneratorLayer &new_layer = layer_allocate(layer_storage, SupporLayerType::RaftInterface); - raft_layers.push_back(&new_layer); - new_layer.print_z = print_z + slicing_params.interface_raft_layer_height; - new_layer.height = slicing_params.interface_raft_layer_height; - new_layer.bottom_z = print_z; - new_layer.polygons = interface_polygons; - //FIXME misusing contact_polygons for support columns. - new_layer.contact_polygons = std::make_unique(columns); - } - } else { - if (columns_base != nullptr) { - // Expand the bases of the support columns in the 1st layer. - Polygons &raft = columns_base->polygons; - Polygons trimming = offset(object.layers().front()->lslices, (float)scale_(support_params.gap_xy), SUPPORT_SURFACES_OFFSET_PARAMETERS); - if (inflate_factor_1st_layer > SCALED_EPSILON) { - // Inflate in multiple steps to avoid leaking of the support 1st layer through object walls. - auto nsteps = std::max(5, int(ceil(inflate_factor_1st_layer / support_params.first_layer_flow.scaled_width()))); - float step = inflate_factor_1st_layer / nsteps; - for (int i = 0; i < nsteps; ++ i) - raft = diff(expand(raft, step), trimming); - } else - raft = diff(raft, trimming); - if (! interface_polygons.empty()) - columns_base->polygons = diff(columns_base->polygons, interface_polygons); - } - if (! brim.empty()) { - if (columns_base) - columns_base->polygons = diff(columns_base->polygons, brim); - if (contacts) - contacts->polygons = diff(contacts->polygons, brim); - if (interfaces) - interfaces->polygons = diff(interfaces->polygons, brim); - if (base_interfaces) - base_interfaces->polygons = diff(base_interfaces->polygons, brim); - } - } - - return raft_layers; -} - // Convert some of the intermediate layers into top/bottom interface layers as well as base interface layers. std::pair PrintObjectSupportMaterial::generate_interface_layers( const SupportGeneratorLayersPtr &bottom_contacts, @@ -3111,9 +2559,8 @@ std::pair PrintObjectSuppo auto smoothing_distance = m_support_params.support_material_interface_flow.scaled_spacing() * 1.5; auto minimum_island_radius = m_support_params.support_material_interface_flow.scaled_spacing() / m_support_params.interface_density; auto closing_distance = smoothing_distance; // scaled(m_object_config->support_material_closing_radius.value); - tbb::spin_mutex layer_storage_mutex; // Insert a new layer into base_interface_layers, if intersection with base exists. - auto insert_layer = [&layer_storage, &layer_storage_mutex, snug_supports, closing_distance, smoothing_distance, minimum_island_radius]( + auto insert_layer = [&layer_storage, snug_supports, closing_distance, smoothing_distance, minimum_island_radius]( SupportGeneratorLayer &intermediate_layer, Polygons &bottom, Polygons &&top, const Polygons *subtract, SupporLayerType type) -> SupportGeneratorLayer* { assert(! bottom.empty() || ! top.empty()); // Merge top into bottom, unite them with a safety offset. @@ -3128,7 +2575,7 @@ std::pair PrintObjectSuppo //FIXME Remove non-printable tiny islands, let them be printed using the base support. //bottom = opening(std::move(bottom), minimum_island_radius); if (! bottom.empty()) { - SupportGeneratorLayer &layer_new = layer_allocate(layer_storage, layer_storage_mutex, type); + SupportGeneratorLayer &layer_new = layer_storage.allocate(type); layer_new.polygons = std::move(bottom); layer_new.print_z = intermediate_layer.print_z; layer_new.bottom_z = intermediate_layer.bottom_z; @@ -3230,1428 +2677,6 @@ std::pair PrintObjectSuppo return base_and_interface_layers; } -static inline void fill_expolygon_generate_paths( - ExtrusionEntitiesPtr &dst, - ExPolygon &&expolygon, - Fill *filler, - const FillParams &fill_params, - float density, - ExtrusionRole role, - const Flow &flow) -{ - Surface surface(stInternal, std::move(expolygon)); - Polylines polylines; - try { - assert(!fill_params.use_arachne); - polylines = filler->fill_surface(&surface, fill_params); - } catch (InfillFailedException &) { - } - extrusion_entities_append_paths( - dst, - std::move(polylines), - role, - flow.mm3_per_mm(), flow.width(), flow.height()); -} - -static inline void fill_expolygons_generate_paths( - ExtrusionEntitiesPtr &dst, - ExPolygons &&expolygons, - Fill *filler, - const FillParams &fill_params, - float density, - ExtrusionRole role, - const Flow &flow) -{ - for (ExPolygon &expoly : expolygons) - fill_expolygon_generate_paths(dst, std::move(expoly), filler, fill_params, density, role, flow); -} - -static inline void fill_expolygons_generate_paths( - ExtrusionEntitiesPtr &dst, - ExPolygons &&expolygons, - Fill *filler, - float density, - ExtrusionRole role, - const Flow &flow) -{ - FillParams fill_params; - fill_params.density = density; - fill_params.dont_adjust = true; - fill_expolygons_generate_paths(dst, std::move(expolygons), filler, fill_params, density, role, flow); -} - -static Polylines draw_perimeters(const ExPolygon &expoly, double clip_length) -{ - // Draw the perimeters. - Polylines polylines; - polylines.reserve(expoly.holes.size() + 1); - for (size_t i = 0; i <= expoly.holes.size(); ++ i) { - Polyline pl(i == 0 ? expoly.contour.points : expoly.holes[i - 1].points); - pl.points.emplace_back(pl.points.front()); - if (i > 0) - // It is a hole, reverse it. - pl.reverse(); - // so that all contours are CCW oriented. - pl.clip_end(clip_length); - polylines.emplace_back(std::move(pl)); - } - return polylines; -} - -static inline void tree_supports_generate_paths( - ExtrusionEntitiesPtr &dst, - const Polygons &polygons, - const Flow &flow) -{ - // Offset expolygon inside, returns number of expolygons collected (0 or 1). - // Vertices of output paths are marked with Z = source contour index of the expoly. - // Vertices at the intersection of source contours are marked with Z = -1. - auto shrink_expolygon_with_contour_idx = [](const Slic3r::ExPolygon &expoly, const float delta, ClipperLib::JoinType joinType, double miterLimit, ClipperLib_Z::Paths &out) -> int - { - assert(delta > 0); - auto append_paths_with_z = [](ClipperLib::Paths &src, coord_t contour_idx, ClipperLib_Z::Paths &dst) { - dst.reserve(next_highest_power_of_2(dst.size() + src.size())); - for (const ClipperLib::Path &contour : src) { - ClipperLib_Z::Path tmp; - tmp.reserve(contour.size()); - for (const Point &p : contour) - tmp.emplace_back(p.x(), p.y(), contour_idx); - dst.emplace_back(std::move(tmp)); - } - }; - - // 1) Offset the outer contour. - ClipperLib_Z::Paths contours; - { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(delta * 0.005); - co.AddPath(expoly.contour.points, joinType, ClipperLib::etClosedPolygon); - ClipperLib::Paths contours_raw; - co.Execute(contours_raw, - delta); - if (contours_raw.empty()) - // No need to try to offset the holes. - return 0; - append_paths_with_z(contours_raw, 0, contours); - } - - if (expoly.holes.empty()) { - // No need to subtract holes from the offsetted expolygon, we are done. - append(out, std::move(contours)); - } else { - // 2) Offset the holes one by one, collect the offsetted holes. - ClipperLib_Z::Paths holes; - { - for (const Polygon &hole : expoly.holes) { - ClipperLib::ClipperOffset co; - if (joinType == jtRound) - co.ArcTolerance = miterLimit; - else - co.MiterLimit = miterLimit; - co.ShortestEdgeLength = double(delta * 0.005); - co.AddPath(hole.points, joinType, ClipperLib::etClosedPolygon); - ClipperLib::Paths out2; - // Execute reorients the contours so that the outer most contour has a positive area. Thus the output - // contours will be CCW oriented even though the input paths are CW oriented. - // Offset is applied after contour reorientation, thus the signum of the offset value is reversed. - co.Execute(out2, delta); - append_paths_with_z(out2, 1 + (&hole - expoly.holes.data()), holes); - } - } - - // 3) Subtract holes from the contours. - if (holes.empty()) { - // No hole remaining after an offset. Just copy the outer contour. - append(out, std::move(contours)); - } else { - // Negative offset. There is a chance, that the offsetted hole intersects the outer contour. - // Subtract the offsetted holes from the offsetted contours. - ClipperLib_Z::Clipper clipper; - clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot, const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) { - //pt.z() = std::max(std::max(e1bot.z(), e1top.z()), std::max(e2bot.z(), e2top.z())); - // Just mark the intersection. - pt.z() = -1; - }); - clipper.AddPaths(contours, ClipperLib_Z::ptSubject, true); - clipper.AddPaths(holes, ClipperLib_Z::ptClip, true); - ClipperLib_Z::Paths output; - clipper.Execute(ClipperLib_Z::ctDifference, output, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero); - if (! output.empty()) { - append(out, std::move(output)); - } else { - // The offsetted holes have eaten up the offsetted outer contour. - return 0; - } - } - } - - return 1; - }; - - const double spacing = flow.scaled_spacing(); - // Clip the sheath path to avoid the extruder to get exactly on the first point of the loop. - const double clip_length = spacing * 0.15; - const double anchor_length = spacing * 6.; - ClipperLib_Z::Paths anchor_candidates; - for (ExPolygon& expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5 * flow.scaled_width()))) { - std::unique_ptr eec; - double area = expoly.area(); - if (area > sqr(scaled(5.))) { - eec = std::make_unique(); - // Don't reoder internal / external loops of the same island, always start with the internal loop. - eec->no_sort = true; - // Make the tree branch stable by adding another perimeter. - ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width()); - if (level2.size() == 1) { - Polylines polylines; - extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), - // Disable reversal of the path, always start with the anchor, always print CCW. - false); - expoly = level2.front(); - } - } - - // Try to produce one more perimeter to place the seam anchor. - // First genrate a 2nd perimeter loop as a source for anchor candidates. - // The anchor candidate points are annotated with an index of the source contour or with -1 if on intersection. - anchor_candidates.clear(); - shrink_expolygon_with_contour_idx(expoly, flow.scaled_width(), DefaultJoinType, 1.2, anchor_candidates); - // Orient all contours CW. - for (auto &path : anchor_candidates) - if (ClipperLib_Z::Area(path) > 0) - std::reverse(path.begin(), path.end()); - - // Draw the perimeters. - Polylines polylines; - polylines.reserve(expoly.holes.size() + 1); - for (size_t idx_loop = 0; idx_loop < expoly.num_contours(); ++ idx_loop) { - // Open the loop with a seam. - const Polygon &loop = expoly.contour_or_hole(idx_loop); - Polyline pl(loop.points); - // Orient all contours CW, because the anchor will be added to the end of polyline while we want to start a loop with the anchor. - if (idx_loop == 0) - // It is an outer contour. - pl.reverse(); - pl.points.emplace_back(pl.points.front()); - pl.clip_end(clip_length); - if (pl.size() < 2) - continue; - // Find the foot of the seam point on anchor_candidates. Only pick an anchor point that was created by offsetting the source contour. - ClipperLib_Z::Path *closest_contour = nullptr; - Vec2d closest_point; - int closest_point_idx = -1; - double closest_point_t; - double d2min = std::numeric_limits::max(); - Vec2d seam_pt = pl.back().cast(); - for (ClipperLib_Z::Path &path : anchor_candidates) - for (int i = 0; i < path.size(); ++ i) { - int j = next_idx_modulo(i, path); - if (path[i].z() == idx_loop || path[j].z() == idx_loop) { - Vec2d pi(path[i].x(), path[i].y()); - Vec2d pj(path[j].x(), path[j].y()); - Vec2d v = pj - pi; - Vec2d w = seam_pt - pi; - auto l2 = v.squaredNorm(); - auto t = std::clamp((l2 == 0) ? 0 : v.dot(w) / l2, 0., 1.); - if ((path[i].z() == idx_loop || t > EPSILON) && (path[j].z() == idx_loop || t < 1. - EPSILON)) { - // Closest point. - Vec2d fp = pi + v * t; - double d2 = (fp - seam_pt).squaredNorm(); - if (d2 < d2min) { - d2min = d2; - closest_contour = &path; - closest_point = fp; - closest_point_idx = i; - closest_point_t = t; - } - } - } - } - if (d2min < sqr(flow.scaled_width() * 3.)) { - // Try to cut an anchor from the closest_contour. - // Both closest_contour and pl are CW oriented. - pl.points.emplace_back(closest_point.cast()); - const ClipperLib_Z::Path &path = *closest_contour; - double remaining_length = anchor_length - (seam_pt - closest_point).norm(); - int i = closest_point_idx; - int j = next_idx_modulo(i, *closest_contour); - Vec2d pi(path[i].x(), path[i].y()); - Vec2d pj(path[j].x(), path[j].y()); - Vec2d v = pj - pi; - double l = v.norm(); - if (remaining_length < (1. - closest_point_t) * l) { - // Just trim the current line. - pl.points.emplace_back((closest_point + v * (remaining_length / l)).cast()); - } else { - // Take the rest of the current line, continue with the other lines. - pl.points.emplace_back(path[j].x(), path[j].y()); - pi = pj; - for (i = j; path[i].z() == idx_loop && remaining_length > 0; i = j, pi = pj) { - j = next_idx_modulo(i, path); - pj = Vec2d(path[j].x(), path[j].y()); - v = pj - pi; - l = v.norm(); - if (i == closest_point_idx) { - // Back at the first segment. Most likely this should not happen and we may end the anchor. - break; - } - if (remaining_length <= l) { - pl.points.emplace_back((pi + v * (remaining_length / l)).cast()); - break; - } - pl.points.emplace_back(path[j].x(), path[j].y()); - remaining_length -= l; - } - } - } - // Start with the anchor. - pl.reverse(); - polylines.emplace_back(std::move(pl)); - } - - ExtrusionEntitiesPtr &out = eec ? eec->entities : dst; - extrusion_entities_append_paths(out, std::move(polylines), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), - // Disable reversal of the path, always start with the anchor, always print CCW. - false); - if (eec) { - std::reverse(eec->entities.begin(), eec->entities.end()); - dst.emplace_back(eec.release()); - } - } -} - -static inline void fill_expolygons_with_sheath_generate_paths( - ExtrusionEntitiesPtr &dst, - const Polygons &polygons, - Fill *filler, - float density, - ExtrusionRole role, - const Flow &flow, - bool with_sheath, - bool no_sort) -{ - if (polygons.empty()) - return; - - if (! with_sheath) { - fill_expolygons_generate_paths(dst, closing_ex(polygons, float(SCALED_EPSILON)), filler, density, role, flow); - return; - } - - FillParams fill_params; - fill_params.density = density; - fill_params.dont_adjust = true; - - const double spacing = flow.scaled_spacing(); - // Clip the sheath path to avoid the extruder to get exactly on the first point of the loop. - const double clip_length = spacing * 0.15; - - for (ExPolygon &expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5*flow.scaled_width()))) { - // Don't reorder the skirt and its infills. - std::unique_ptr eec; - if (no_sort) { - eec = std::make_unique(); - eec->no_sort = true; - } - ExtrusionEntitiesPtr &out = no_sort ? eec->entities : dst; - extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height()); - // Fill in the rest. - fill_expolygons_generate_paths(out, offset_ex(expoly, float(-0.4 * spacing)), filler, fill_params, density, role, flow); - if (no_sort && ! eec->empty()) - dst.emplace_back(eec.release()); - } -} - -// Support layers, partially processed. -struct SupportGeneratorLayerExtruded -{ - SupportGeneratorLayerExtruded& operator=(SupportGeneratorLayerExtruded &&rhs) { - this->layer = rhs.layer; - this->extrusions = std::move(rhs.extrusions); - m_polygons_to_extrude = std::move(rhs.m_polygons_to_extrude); - rhs.layer = nullptr; - return *this; - } - - bool empty() const { - return layer == nullptr || layer->polygons.empty(); - } - - void set_polygons_to_extrude(Polygons &&polygons) { - if (m_polygons_to_extrude == nullptr) - m_polygons_to_extrude = std::make_unique(std::move(polygons)); - else - *m_polygons_to_extrude = std::move(polygons); - } - Polygons& polygons_to_extrude() { return (m_polygons_to_extrude == nullptr) ? layer->polygons : *m_polygons_to_extrude; } - const Polygons& polygons_to_extrude() const { return (m_polygons_to_extrude == nullptr) ? layer->polygons : *m_polygons_to_extrude; } - - bool could_merge(const SupportGeneratorLayerExtruded &other) const { - return ! this->empty() && ! other.empty() && - std::abs(this->layer->height - other.layer->height) < EPSILON && - this->layer->bridging == other.layer->bridging; - } - - // Merge regions, perform boolean union over the merged polygons. - void merge(SupportGeneratorLayerExtruded &&other) { - assert(this->could_merge(other)); - // 1) Merge the rest polygons to extrude, if there are any. - if (other.m_polygons_to_extrude != nullptr) { - if (m_polygons_to_extrude == nullptr) { - // This layer has no extrusions generated yet, if it has no m_polygons_to_extrude (its area to extrude was not reduced yet). - assert(this->extrusions.empty()); - m_polygons_to_extrude = std::make_unique(this->layer->polygons); - } - Slic3r::polygons_append(*m_polygons_to_extrude, std::move(*other.m_polygons_to_extrude)); - *m_polygons_to_extrude = union_safety_offset(*m_polygons_to_extrude); - other.m_polygons_to_extrude.reset(); - } else if (m_polygons_to_extrude != nullptr) { - assert(other.m_polygons_to_extrude == nullptr); - // The other layer has no extrusions generated yet, if it has no m_polygons_to_extrude (its area to extrude was not reduced yet). - assert(other.extrusions.empty()); - Slic3r::polygons_append(*m_polygons_to_extrude, other.layer->polygons); - *m_polygons_to_extrude = union_safety_offset(*m_polygons_to_extrude); - } - // 2) Merge the extrusions. - this->extrusions.insert(this->extrusions.end(), other.extrusions.begin(), other.extrusions.end()); - other.extrusions.clear(); - // 3) Merge the infill polygons. - Slic3r::polygons_append(this->layer->polygons, std::move(other.layer->polygons)); - this->layer->polygons = union_safety_offset(this->layer->polygons); - other.layer->polygons.clear(); - } - - void polygons_append(Polygons &dst) const { - if (layer != NULL && ! layer->polygons.empty()) - Slic3r::polygons_append(dst, layer->polygons); - } - - // The source layer. It carries the height and extrusion type (bridging / non bridging, extrusion height). - SupportGeneratorLayer *layer { nullptr }; - // Collect extrusions. They will be exported sorted by the bottom height. - ExtrusionEntitiesPtr extrusions; - -private: - // In case the extrusions are non-empty, m_polygons_to_extrude may contain the rest areas yet to be filled by additional support. - // This is useful mainly for the loop interfaces, which are generated before the zig-zag infills. - std::unique_ptr m_polygons_to_extrude; -}; - -typedef std::vector SupportGeneratorLayerExtrudedPtrs; - -struct LoopInterfaceProcessor -{ - LoopInterfaceProcessor(coordf_t circle_r) : - n_contact_loops(0), - circle_radius(circle_r), - circle_distance(circle_r * 3.) - { - // Shape of the top contact area. - circle.points.reserve(6); - for (size_t i = 0; i < 6; ++ i) { - double angle = double(i) * M_PI / 3.; - circle.points.push_back(Point(circle_radius * cos(angle), circle_radius * sin(angle))); - } - } - - // Generate loop contacts at the top_contact_layer, - // trim the top_contact_layer->polygons with the areas covered by the loops. - void generate(SupportGeneratorLayerExtruded &top_contact_layer, const Flow &interface_flow_src) const; - - int n_contact_loops; - coordf_t circle_radius; - coordf_t circle_distance; - Polygon circle; -}; - -void LoopInterfaceProcessor::generate(SupportGeneratorLayerExtruded &top_contact_layer, const Flow &interface_flow_src) const -{ - if (n_contact_loops == 0 || top_contact_layer.empty()) - return; - - Flow flow = interface_flow_src.with_height(top_contact_layer.layer->height); - - Polygons overhang_polygons; - if (top_contact_layer.layer->overhang_polygons != nullptr) - overhang_polygons = std::move(*top_contact_layer.layer->overhang_polygons); - - // Generate the outermost loop. - // Find centerline of the external loop (or any other kind of extrusions should the loop be skipped) - ExPolygons top_contact_expolygons = offset_ex(union_ex(top_contact_layer.layer->polygons), - 0.5f * flow.scaled_width()); - - // Grid size and bit shifts for quick and exact to/from grid coordinates manipulation. - coord_t circle_grid_resolution = 1; - coord_t circle_grid_powerof2 = 0; - { - // epsilon to account for rounding errors - coord_t circle_grid_resolution_non_powerof2 = coord_t(2. * circle_distance + 3.); - while (circle_grid_resolution < circle_grid_resolution_non_powerof2) { - circle_grid_resolution <<= 1; - ++ circle_grid_powerof2; - } - } - - struct PointAccessor { - const Point* operator()(const Point &pt) const { return &pt; } - }; - typedef ClosestPointInRadiusLookup ClosestPointLookupType; - - Polygons loops0; - { - // find centerline of the external loop of the contours - // Only consider the loops facing the overhang. - Polygons external_loops; - // Holes in the external loops. - Polygons circles; - Polygons overhang_with_margin = offset(union_ex(overhang_polygons), 0.5f * flow.scaled_width()); - for (ExPolygons::iterator it_contact_expoly = top_contact_expolygons.begin(); it_contact_expoly != top_contact_expolygons.end(); ++ it_contact_expoly) { - // Store the circle centers placed for an expolygon into a regular grid, hashed by the circle centers. - ClosestPointLookupType circle_centers_lookup(coord_t(circle_distance - SCALED_EPSILON)); - Points circle_centers; - Point center_last; - // For each contour of the expolygon, start with the outer contour, continue with the holes. - for (size_t i_contour = 0; i_contour <= it_contact_expoly->holes.size(); ++ i_contour) { - Polygon &contour = (i_contour == 0) ? it_contact_expoly->contour : it_contact_expoly->holes[i_contour - 1]; - const Point *seg_current_pt = nullptr; - coordf_t seg_current_t = 0.; - if (! intersection_pl(contour.split_at_first_point(), overhang_with_margin).empty()) { - // The contour is below the overhang at least to some extent. - //FIXME ideally one would place the circles below the overhang only. - // Walk around the contour and place circles so their centers are not closer than circle_distance from each other. - if (circle_centers.empty()) { - // Place the first circle. - seg_current_pt = &contour.points.front(); - seg_current_t = 0.; - center_last = *seg_current_pt; - circle_centers_lookup.insert(center_last); - circle_centers.push_back(center_last); - } - for (Points::const_iterator it = contour.points.begin() + 1; it != contour.points.end(); ++it) { - // Is it possible to place a circle on this segment? Is it not too close to any of the circles already placed on this contour? - const Point &p1 = *(it-1); - const Point &p2 = *it; - // Intersection of a ray (p1, p2) with a circle placed at center_last, with radius of circle_distance. - const Vec2d v_seg(coordf_t(p2(0)) - coordf_t(p1(0)), coordf_t(p2(1)) - coordf_t(p1(1))); - const Vec2d v_cntr(coordf_t(p1(0) - center_last(0)), coordf_t(p1(1) - center_last(1))); - coordf_t a = v_seg.squaredNorm(); - coordf_t b = 2. * v_seg.dot(v_cntr); - coordf_t c = v_cntr.squaredNorm() - circle_distance * circle_distance; - coordf_t disc = b * b - 4. * a * c; - if (disc > 0.) { - // The circle intersects a ray. Avoid the parts of the segment inside the circle. - coordf_t t1 = (-b - sqrt(disc)) / (2. * a); - coordf_t t2 = (-b + sqrt(disc)) / (2. * a); - coordf_t t0 = (seg_current_pt == &p1) ? seg_current_t : 0.; - // Take the lowest t in , excluding . - coordf_t t; - if (t0 <= t1) - t = t0; - else if (t2 <= 1.) - t = t2; - else { - // Try the following segment. - seg_current_pt = nullptr; - continue; - } - seg_current_pt = &p1; - seg_current_t = t; - center_last = Point(p1(0) + coord_t(v_seg(0) * t), p1(1) + coord_t(v_seg(1) * t)); - // It has been verified that the new point is far enough from center_last. - // Ensure, that it is far enough from all the centers. - std::pair circle_closest = circle_centers_lookup.find(center_last); - if (circle_closest.first != nullptr) { - -- it; - continue; - } - } else { - // All of the segment is outside the circle. Take the first point. - seg_current_pt = &p1; - seg_current_t = 0.; - center_last = p1; - } - // Place the first circle. - circle_centers_lookup.insert(center_last); - circle_centers.push_back(center_last); - } - external_loops.push_back(std::move(contour)); - for (const Point ¢er : circle_centers) { - circles.push_back(circle); - circles.back().translate(center); - } - } - } - } - // Apply a pattern to the external loops. - loops0 = diff(external_loops, circles); - } - - Polylines loop_lines; - { - // make more loops - Polygons loop_polygons = loops0; - for (int i = 1; i < n_contact_loops; ++ i) - polygons_append(loop_polygons, - opening( - loops0, - i * flow.scaled_spacing() + 0.5f * flow.scaled_spacing(), - 0.5f * flow.scaled_spacing())); - // Clip such loops to the side oriented towards the object. - // Collect split points, so they will be recognized after the clipping. - // At the split points the clipped pieces will be stitched back together. - loop_lines.reserve(loop_polygons.size()); - std::unordered_map map_split_points; - for (Polygons::const_iterator it = loop_polygons.begin(); it != loop_polygons.end(); ++ it) { - assert(map_split_points.find(it->first_point()) == map_split_points.end()); - map_split_points[it->first_point()] = -1; - loop_lines.push_back(it->split_at_first_point()); - } - loop_lines = intersection_pl(loop_lines, expand(overhang_polygons, scale_(SUPPORT_MATERIAL_MARGIN))); - // Because a closed loop has been split to a line, loop_lines may contain continuous segments split to 2 pieces. - // Try to connect them. - for (int i_line = 0; i_line < int(loop_lines.size()); ++ i_line) { - Polyline &polyline = loop_lines[i_line]; - auto it = map_split_points.find(polyline.first_point()); - if (it != map_split_points.end()) { - // This is a stitching point. - // If this assert triggers, multiple source polygons likely intersected at this point. - assert(it->second != -2); - if (it->second < 0) { - // First occurence. - it->second = i_line; - } else { - // Second occurence. Join the lines. - Polyline &polyline_1st = loop_lines[it->second]; - assert(polyline_1st.first_point() == it->first || polyline_1st.last_point() == it->first); - if (polyline_1st.first_point() == it->first) - polyline_1st.reverse(); - polyline_1st.append(std::move(polyline)); - it->second = -2; - } - continue; - } - it = map_split_points.find(polyline.last_point()); - if (it != map_split_points.end()) { - // This is a stitching point. - // If this assert triggers, multiple source polygons likely intersected at this point. - assert(it->second != -2); - if (it->second < 0) { - // First occurence. - it->second = i_line; - } else { - // Second occurence. Join the lines. - Polyline &polyline_1st = loop_lines[it->second]; - assert(polyline_1st.first_point() == it->first || polyline_1st.last_point() == it->first); - if (polyline_1st.first_point() == it->first) - polyline_1st.reverse(); - polyline.reverse(); - polyline_1st.append(std::move(polyline)); - it->second = -2; - } - } - } - // Remove empty lines. - remove_degenerate(loop_lines); - } - - // add the contact infill area to the interface area - // note that growing loops by $circle_radius ensures no tiny - // extrusions are left inside the circles; however it creates - // a very large gap between loops and contact_infill_polygons, so maybe another - // solution should be found to achieve both goals - // Store the trimmed polygons into a separate polygon set, so the original infill area remains intact for - // "modulate by layer thickness". - top_contact_layer.set_polygons_to_extrude(diff(top_contact_layer.layer->polygons, offset(loop_lines, float(circle_radius * 1.1)))); - - // Transform loops into ExtrusionPath objects. - extrusion_entities_append_paths( - top_contact_layer.extrusions, - std::move(loop_lines), - ExtrusionRole::SupportMaterialInterface, flow.mm3_per_mm(), flow.width(), flow.height()); -} - -#ifdef SLIC3R_DEBUG -static std::string dbg_index_to_color(int idx) -{ - if (idx < 0) - return "yellow"; - idx = idx % 3; - switch (idx) { - case 0: return "red"; - case 1: return "green"; - default: return "blue"; - } -} -#endif /* SLIC3R_DEBUG */ - -// When extruding a bottom interface layer over an object, the bottom interface layer is extruded in a thin air, therefore -// it is being extruded with a bridging flow to not shrink excessively (the die swell effect). -// Tiny extrusions are better avoided and it is always better to anchor the thread to an existing support structure if possible. -// Therefore the bottom interface spots are expanded a bit. The expanded regions may overlap with another bottom interface layers, -// leading to over extrusion, where they overlap. The over extrusion is better avoided as it often makes the interface layers -// to stick too firmly to the object. -// -// Modulate thickness (increase bottom_z) of extrusions_in_out generated for this_layer -// if they overlap with overlapping_layers, whose print_z is above this_layer.bottom_z() and below this_layer.print_z. -void modulate_extrusion_by_overlapping_layers( - // Extrusions generated for this_layer. - ExtrusionEntitiesPtr &extrusions_in_out, - const SupportGeneratorLayer &this_layer, - // Multiple layers overlapping with this_layer, sorted bottom up. - const SupportGeneratorLayersPtr &overlapping_layers) -{ - size_t n_overlapping_layers = overlapping_layers.size(); - if (n_overlapping_layers == 0 || extrusions_in_out.empty()) - // The extrusions do not overlap with any other extrusion. - return; - - // Get the initial extrusion parameters. - ExtrusionPath *extrusion_path_template = dynamic_cast(extrusions_in_out.front()); - assert(extrusion_path_template != nullptr); - ExtrusionRole extrusion_role = extrusion_path_template->role(); - float extrusion_width = extrusion_path_template->width; - - struct ExtrusionPathFragment - { - ExtrusionPathFragment() : mm3_per_mm(-1), width(-1), height(-1) {}; - ExtrusionPathFragment(double mm3_per_mm, float width, float height) : mm3_per_mm(mm3_per_mm), width(width), height(height) {}; - - Polylines polylines; - double mm3_per_mm; - float width; - float height; - }; - - // Split the extrusions by the overlapping layers, reduce their extrusion rate. - // The last path_fragment is from this_layer. - std::vector path_fragments( - n_overlapping_layers + 1, - ExtrusionPathFragment(extrusion_path_template->mm3_per_mm, extrusion_path_template->width, extrusion_path_template->height)); - // Don't use it, it will be released. - extrusion_path_template = nullptr; - -#ifdef SLIC3R_DEBUG - static int iRun = 0; - ++ iRun; - BoundingBox bbox; - for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { - const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; - bbox.merge(get_extents(overlapping_layer.polygons)); - } - for (ExtrusionEntitiesPtr::const_iterator it = extrusions_in_out.begin(); it != extrusions_in_out.end(); ++ it) { - ExtrusionPath *path = dynamic_cast(*it); - assert(path != nullptr); - bbox.merge(get_extents(path->polyline)); - } - SVG svg(debug_out_path("support-fragments-%d-%lf.svg", iRun, this_layer.print_z).c_str(), bbox); - const float transparency = 0.5f; - // Filled polygons for the overlapping regions. - svg.draw(union_ex(this_layer.polygons), dbg_index_to_color(-1), transparency); - for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { - const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; - svg.draw(union_ex(overlapping_layer.polygons), dbg_index_to_color(int(i_overlapping_layer)), transparency); - } - // Contours of the overlapping regions. - svg.draw(to_polylines(this_layer.polygons), dbg_index_to_color(-1), scale_(0.2)); - for (size_t i_overlapping_layer = 0; i_overlapping_layer < n_overlapping_layers; ++ i_overlapping_layer) { - const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; - svg.draw(to_polylines(overlapping_layer.polygons), dbg_index_to_color(int(i_overlapping_layer)), scale_(0.1)); - } - // Fill extrusion, the source. - for (ExtrusionEntitiesPtr::const_iterator it = extrusions_in_out.begin(); it != extrusions_in_out.end(); ++ it) { - ExtrusionPath *path = dynamic_cast(*it); - std::string color_name; - switch ((it - extrusions_in_out.begin()) % 9) { - case 0: color_name = "magenta"; break; - case 1: color_name = "deepskyblue"; break; - case 2: color_name = "coral"; break; - case 3: color_name = "goldenrod"; break; - case 4: color_name = "orange"; break; - case 5: color_name = "olivedrab"; break; - case 6: color_name = "blueviolet"; break; - case 7: color_name = "brown"; break; - default: color_name = "orchid"; break; - } - svg.draw(path->polyline, color_name, scale_(0.2)); - } -#endif /* SLIC3R_DEBUG */ - - // End points of the original paths. - std::vector> path_ends; - // Collect the paths of this_layer. - { - Polylines &polylines = path_fragments.back().polylines; - for (ExtrusionEntity *ee : extrusions_in_out) { - ExtrusionPath *path = dynamic_cast(ee); - assert(path != nullptr); - polylines.emplace_back(Polyline(std::move(path->polyline))); - path_ends.emplace_back(std::pair(polylines.back().points.front(), polylines.back().points.back())); - delete path; - } - } - // Destroy the original extrusion paths, their polylines were moved to path_fragments already. - // This will be the destination for the new paths. - extrusions_in_out.clear(); - - // Fragment the path segments by overlapping layers. The overlapping layers are sorted by an increasing print_z. - // Trim by the highest overlapping layer first. - for (int i_overlapping_layer = int(n_overlapping_layers) - 1; i_overlapping_layer >= 0; -- i_overlapping_layer) { - const SupportGeneratorLayer &overlapping_layer = *overlapping_layers[i_overlapping_layer]; - ExtrusionPathFragment &frag = path_fragments[i_overlapping_layer]; - Polygons polygons_trimming = offset(union_ex(overlapping_layer.polygons), float(scale_(0.5*extrusion_width))); - frag.polylines = intersection_pl(path_fragments.back().polylines, polygons_trimming); - path_fragments.back().polylines = diff_pl(path_fragments.back().polylines, polygons_trimming); - // Adjust the extrusion parameters for a reduced layer height and a non-bridging flow (nozzle_dmr = -1, does not matter). - assert(this_layer.print_z > overlapping_layer.print_z); - frag.height = float(this_layer.print_z - overlapping_layer.print_z); - frag.mm3_per_mm = Flow(frag.width, frag.height, -1.f).mm3_per_mm(); -#ifdef SLIC3R_DEBUG - svg.draw(frag.polylines, dbg_index_to_color(i_overlapping_layer), scale_(0.1)); -#endif /* SLIC3R_DEBUG */ - } - -#ifdef SLIC3R_DEBUG - svg.draw(path_fragments.back().polylines, dbg_index_to_color(-1), scale_(0.1)); - svg.Close(); -#endif /* SLIC3R_DEBUG */ - - // Now chain the split segments using hashing and a nearly exact match, maintaining the order of segments. - // Create a single ExtrusionPath or ExtrusionEntityCollection per source ExtrusionPath. - // Map of fragment start/end points to a pair of - // Because a non-exact matching is used for the end points, a multi-map is used. - // As the clipper library may reverse the order of some clipped paths, store both ends into the map. - struct ExtrusionPathFragmentEnd - { - ExtrusionPathFragmentEnd(size_t alayer_idx, size_t apolyline_idx, bool ais_start) : - layer_idx(alayer_idx), polyline_idx(apolyline_idx), is_start(ais_start) {} - size_t layer_idx; - size_t polyline_idx; - bool is_start; - }; - class ExtrusionPathFragmentEndPointAccessor { - public: - ExtrusionPathFragmentEndPointAccessor(const std::vector &path_fragments) : m_path_fragments(path_fragments) {} - // Return an end point of a fragment, or nullptr if the fragment has been consumed already. - const Point* operator()(const ExtrusionPathFragmentEnd &fragment_end) const { - const Polyline &polyline = m_path_fragments[fragment_end.layer_idx].polylines[fragment_end.polyline_idx]; - return polyline.points.empty() ? nullptr : - (fragment_end.is_start ? &polyline.points.front() : &polyline.points.back()); - } - private: - ExtrusionPathFragmentEndPointAccessor& operator=(const ExtrusionPathFragmentEndPointAccessor&) { - return *this; - } - - const std::vector &m_path_fragments; - }; - const coord_t search_radius = 7; - ClosestPointInRadiusLookup map_fragment_starts( - search_radius, ExtrusionPathFragmentEndPointAccessor(path_fragments)); - for (size_t i_overlapping_layer = 0; i_overlapping_layer <= n_overlapping_layers; ++ i_overlapping_layer) { - const Polylines &polylines = path_fragments[i_overlapping_layer].polylines; - for (size_t i_polyline = 0; i_polyline < polylines.size(); ++ i_polyline) { - // Map a starting point of a polyline to a pair of - if (polylines[i_polyline].points.size() >= 2) { - map_fragment_starts.insert(ExtrusionPathFragmentEnd(i_overlapping_layer, i_polyline, true)); - map_fragment_starts.insert(ExtrusionPathFragmentEnd(i_overlapping_layer, i_polyline, false)); - } - } - } - - // For each source path: - for (size_t i_path = 0; i_path < path_ends.size(); ++ i_path) { - const Point &pt_start = path_ends[i_path].first; - const Point &pt_end = path_ends[i_path].second; - Point pt_current = pt_start; - // Find a chain of fragments with the original / reduced print height. - ExtrusionMultiPath multipath; - for (;;) { - // Find a closest end point to pt_current. - std::pair end_and_dist2 = map_fragment_starts.find(pt_current); - // There may be a bug in Clipper flipping the order of two last points in a fragment? - // assert(end_and_dist2.first != nullptr); - assert(end_and_dist2.first == nullptr || end_and_dist2.second < search_radius * search_radius); - if (end_and_dist2.first == nullptr) { - // New fragment connecting to pt_current was not found. - // Verify that the last point found is close to the original end point of the unfragmented path. - //const double d2 = (pt_end - pt_current).cast.squaredNorm(); - //assert(d2 < coordf_t(search_radius * search_radius)); - // End of the path. - break; - } - const ExtrusionPathFragmentEnd &fragment_end_min = *end_and_dist2.first; - // Fragment to consume. - ExtrusionPathFragment &frag = path_fragments[fragment_end_min.layer_idx]; - Polyline &frag_polyline = frag.polylines[fragment_end_min.polyline_idx]; - // Path to append the fragment to. - ExtrusionPath *path = multipath.paths.empty() ? nullptr : &multipath.paths.back(); - if (path != nullptr) { - // Verify whether the path is compatible with the current fragment. - assert(this_layer.layer_type == SupporLayerType::BottomContact || path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm); - if (path->height != frag.height || path->mm3_per_mm != frag.mm3_per_mm) { - path = nullptr; - } - // Merging with the previous path. This can only happen if the current layer was reduced by a base layer, which was split into a base and interface layer. - } - if (path == nullptr) { - // Allocate a new path. - multipath.paths.push_back(ExtrusionPath(extrusion_role, frag.mm3_per_mm, frag.width, frag.height)); - path = &multipath.paths.back(); - } - // The Clipper library may flip the order of the clipped polylines arbitrarily. - // Reverse the source polyline, if connecting to the end. - if (! fragment_end_min.is_start) - frag_polyline.reverse(); - // Enforce exact overlap of the end points of successive fragments. - assert(frag_polyline.points.front() == pt_current); - frag_polyline.points.front() = pt_current; - // Don't repeat the first point. - if (! path->polyline.points.empty()) - path->polyline.points.pop_back(); - // Consume the fragment's polyline, remove it from the input fragments, so it will be ignored the next time. - path->polyline.append(std::move(frag_polyline)); - frag_polyline.points.clear(); - pt_current = path->polyline.points.back(); - if (pt_current == pt_end) { - // End of the path. - break; - } - } - if (!multipath.paths.empty()) { - if (multipath.paths.size() == 1) { - // This path was not fragmented. - extrusions_in_out.push_back(new ExtrusionPath(std::move(multipath.paths.front()))); - } else { - // This path was fragmented. Copy the collection as a whole object, so the order inside the collection will not be changed - // during the chaining of extrusions_in_out. - extrusions_in_out.push_back(new ExtrusionMultiPath(std::move(multipath))); - } - } - } - // If there are any non-consumed fragments, add them separately. - //FIXME this shall not happen, if the Clipper works as expected and all paths split to fragments could be re-connected. - for (auto it_fragment = path_fragments.begin(); it_fragment != path_fragments.end(); ++ it_fragment) - extrusion_entities_append_paths(extrusions_in_out, std::move(it_fragment->polylines), extrusion_role, it_fragment->mm3_per_mm, it_fragment->width, it_fragment->height); -} - -SupportGeneratorLayersPtr generate_support_layers( - PrintObject &object, - const SupportGeneratorLayersPtr &raft_layers, - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &intermediate_layers, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers) -{ - // Install support layers into the object. - // A support layer installed on a PrintObject has a unique print_z. - SupportGeneratorLayersPtr layers_sorted; - layers_sorted.reserve(raft_layers.size() + bottom_contacts.size() + top_contacts.size() + intermediate_layers.size() + interface_layers.size() + base_interface_layers.size()); - layers_append(layers_sorted, raft_layers); - layers_append(layers_sorted, bottom_contacts); - layers_append(layers_sorted, top_contacts); - layers_append(layers_sorted, intermediate_layers); - layers_append(layers_sorted, interface_layers); - layers_append(layers_sorted, base_interface_layers); - // Sort the layers lexicographically by a raising print_z and a decreasing height. - std::sort(layers_sorted.begin(), layers_sorted.end(), [](auto *l1, auto *l2) { return *l1 < *l2; }); - int layer_id = 0; - int layer_id_interface = 0; - assert(object.support_layers().empty()); - for (size_t i = 0; i < layers_sorted.size();) { - // Find the last layer with roughly the same print_z, find the minimum layer height of all. - // Due to the floating point inaccuracies, the print_z may not be the same even if in theory they should. - size_t j = i + 1; - coordf_t zmax = layers_sorted[i]->print_z + EPSILON; - for (; j < layers_sorted.size() && layers_sorted[j]->print_z <= zmax; ++j) ; - // Assign an average print_z to the set of layers with nearly equal print_z. - coordf_t zavg = 0.5 * (layers_sorted[i]->print_z + layers_sorted[j - 1]->print_z); - coordf_t height_min = layers_sorted[i]->height; - bool empty = true; - // For snug supports, layers where the direction of the support interface shall change are accounted for. - size_t num_interfaces = 0; - size_t num_top_contacts = 0; - double top_contact_bottom_z = 0; - for (size_t u = i; u < j; ++u) { - SupportGeneratorLayer &layer = *layers_sorted[u]; - if (! layer.polygons.empty()) { - empty = false; - num_interfaces += one_of(layer.layer_type, support_types_interface); - if (layer.layer_type == SupporLayerType::TopContact) { - ++ num_top_contacts; - assert(num_top_contacts <= 1); - // All top contact layers sharing this print_z shall also share bottom_z. - //assert(num_top_contacts == 1 || (top_contact_bottom_z - layer.bottom_z) < EPSILON); - top_contact_bottom_z = layer.bottom_z; - } - } - layer.print_z = zavg; - height_min = std::min(height_min, layer.height); - } - if (! empty) { - // Here the upper_layer and lower_layer pointers are left to null at the support layers, - // as they are never used. These pointers are candidates for removal. - bool this_layer_contacts_only = num_top_contacts > 0 && num_top_contacts == num_interfaces; - size_t this_layer_id_interface = layer_id_interface; - if (this_layer_contacts_only) { - // Find a supporting layer for its interface ID. - for (auto it = object.support_layers().rbegin(); it != object.support_layers().rend(); ++ it) - if (const SupportLayer &other_layer = **it; std::abs(other_layer.print_z - top_contact_bottom_z) < EPSILON) { - // other_layer supports this top contact layer. Assign a different support interface direction to this layer - // from the layer that supports it. - this_layer_id_interface = other_layer.interface_id() + 1; - } - } - object.add_support_layer(layer_id ++, this_layer_id_interface, height_min, zavg); - if (num_interfaces && ! this_layer_contacts_only) - ++ layer_id_interface; - } - i = j; - } - return layers_sorted; -} - -void generate_support_toolpaths( - SupportLayerPtrs &support_layers, - const PrintObjectConfig &config, - const SupportParameters &support_params, - const SlicingParameters &slicing_params, - const SupportGeneratorLayersPtr &raft_layers, - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &intermediate_layers, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers) -{ - // loop_interface_processor with a given circle radius. - LoopInterfaceProcessor loop_interface_processor(1.5 * support_params.support_material_interface_flow.scaled_width()); - loop_interface_processor.n_contact_loops = config.support_material_interface_contact_loops ? 1 : 0; - - std::vector angles { support_params.base_angle }; - if (config.support_material_pattern == smpRectilinearGrid) - angles.push_back(support_params.interface_angle); - - BoundingBox bbox_object(Point(-scale_(1.), -scale_(1.0)), Point(scale_(1.), scale_(1.))); - -// const coordf_t link_max_length_factor = 3.; - const coordf_t link_max_length_factor = 0.; - - // Insert the raft base layers. - auto n_raft_layers = std::min(support_layers.size(), std::max(0, int(slicing_params.raft_layers()) - 1)); - - tbb::parallel_for(tbb::blocked_range(0, n_raft_layers), - [&support_layers, &raft_layers, &intermediate_layers, &config, &support_params, &slicing_params, - &bbox_object, link_max_length_factor] - (const tbb::blocked_range& range) { - for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) - { - assert(support_layer_id < raft_layers.size()); - SupportLayer &support_layer = *support_layers[support_layer_id]; - assert(support_layer.support_fills.entities.empty()); - SupportGeneratorLayer &raft_layer = *raft_layers[support_layer_id]; - - std::unique_ptr filler_interface = std::unique_ptr(Fill::new_from_type(support_params.raft_interface_fill_pattern)); - std::unique_ptr filler_support = std::unique_ptr(Fill::new_from_type(support_params.base_fill_pattern)); - filler_interface->set_bounding_box(bbox_object); - filler_support->set_bounding_box(bbox_object); - - // Print the tree supports cutting through the raft with the exception of the 1st layer, where a full support layer will be printed below - // both the raft and the trees. - // Trim the raft layers with the tree polygons. - const Polygons &tree_polygons = - support_layer_id > 0 && support_layer_id < intermediate_layers.size() && is_approx(intermediate_layers[support_layer_id]->print_z, support_layer.print_z) ? - intermediate_layers[support_layer_id]->polygons : Polygons(); - - // Print the support base below the support columns, or the support base for the support columns plus the contacts. - if (support_layer_id > 0) { - const Polygons &to_infill_polygons = (support_layer_id < slicing_params.base_raft_layers) ? - raft_layer.polygons : - //FIXME misusing contact_polygons for support columns. - ((raft_layer.contact_polygons == nullptr) ? Polygons() : *raft_layer.contact_polygons); - // Trees may cut through the raft layers down to a print bed. - Flow flow(float(support_params.support_material_flow.width()), float(raft_layer.height), support_params.support_material_flow.nozzle_diameter()); - assert(!raft_layer.bridging); - if (! to_infill_polygons.empty()) { - Fill *filler = filler_support.get(); - filler->angle = support_params.raft_angle_base; - filler->spacing = support_params.support_material_flow.spacing(); - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.support_density)); - fill_expolygons_with_sheath_generate_paths( - // Destination - support_layer.support_fills.entities, - // Regions to fill - tree_polygons.empty() ? to_infill_polygons : diff(to_infill_polygons, tree_polygons), - // Filler and its parameters - filler, float(support_params.support_density), - // Extrusion parameters - ExtrusionRole::SupportMaterial, flow, - support_params.with_sheath, false); - } - if (! tree_polygons.empty()) - tree_supports_generate_paths(support_layer.support_fills.entities, tree_polygons, flow); - } - - Fill *filler = filler_interface.get(); - Flow flow = support_params.first_layer_flow; - float density = 0.f; - if (support_layer_id == 0) { - // Base flange. - filler->angle = support_params.raft_angle_1st_layer; - filler->spacing = support_params.first_layer_flow.spacing(); - density = float(config.raft_first_layer_density.value * 0.01); - } else if (support_layer_id >= slicing_params.base_raft_layers) { - filler->angle = support_params.raft_interface_angle(support_layer.interface_id()); - // We don't use $base_flow->spacing because we need a constant spacing - // value that guarantees that all layers are correctly aligned. - filler->spacing = support_params.support_material_flow.spacing(); - assert(! raft_layer.bridging); - flow = Flow(float(support_params.raft_interface_flow.width()), float(raft_layer.height), support_params.raft_interface_flow.nozzle_diameter()); - density = float(support_params.raft_interface_density); - } else - continue; - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); - fill_expolygons_with_sheath_generate_paths( - // Destination - support_layer.support_fills.entities, - // Regions to fill - tree_polygons.empty() ? raft_layer.polygons : diff(raft_layer.polygons, tree_polygons), - // Filler and its parameters - filler, density, - // Extrusion parameters - (support_layer_id < slicing_params.base_raft_layers) ? ExtrusionRole::SupportMaterial : ExtrusionRole::SupportMaterialInterface, flow, - // sheath at first layer - support_layer_id == 0, support_layer_id == 0); - } - }); - - struct LayerCacheItem { - LayerCacheItem(SupportGeneratorLayerExtruded *layer_extruded = nullptr) : layer_extruded(layer_extruded) {} - SupportGeneratorLayerExtruded *layer_extruded; - std::vector overlapping; - }; - struct LayerCache { - SupportGeneratorLayerExtruded bottom_contact_layer; - SupportGeneratorLayerExtruded top_contact_layer; - SupportGeneratorLayerExtruded base_layer; - SupportGeneratorLayerExtruded interface_layer; - SupportGeneratorLayerExtruded base_interface_layer; - boost::container::static_vector nonempty; - - void add_nonempty_and_sort() { - for (SupportGeneratorLayerExtruded *item : { &bottom_contact_layer, &top_contact_layer, &interface_layer, &base_interface_layer, &base_layer }) - if (! item->empty()) - this->nonempty.emplace_back(item); - // Sort the layers with the same print_z coordinate by their heights, thickest first. - std::stable_sort(this->nonempty.begin(), this->nonempty.end(), [](const LayerCacheItem &lc1, const LayerCacheItem &lc2) { return lc1.layer_extruded->layer->height > lc2.layer_extruded->layer->height; }); - } - }; - std::vector layer_caches(support_layers.size()); - - tbb::parallel_for(tbb::blocked_range(n_raft_layers, support_layers.size()), - [&config, &slicing_params, &support_params, &support_layers, &bottom_contacts, &top_contacts, &intermediate_layers, &interface_layers, &base_interface_layers, &layer_caches, &loop_interface_processor, - &bbox_object, &angles, n_raft_layers, link_max_length_factor] - (const tbb::blocked_range& range) { - // Indices of the 1st layer in their respective container at the support layer height. - size_t idx_layer_bottom_contact = size_t(-1); - size_t idx_layer_top_contact = size_t(-1); - size_t idx_layer_intermediate = size_t(-1); - size_t idx_layer_interface = size_t(-1); - size_t idx_layer_base_interface = size_t(-1); - const auto fill_type_first_layer = ipRectilinear; - auto filler_interface = std::unique_ptr(Fill::new_from_type(support_params.contact_fill_pattern)); - // Filler for the 1st layer interface, if different from filler_interface. - auto filler_first_layer_ptr = std::unique_ptr(range.begin() == 0 && support_params.contact_fill_pattern != fill_type_first_layer ? Fill::new_from_type(fill_type_first_layer) : nullptr); - // Pointer to the 1st layer interface filler. - auto filler_first_layer = filler_first_layer_ptr ? filler_first_layer_ptr.get() : filler_interface.get(); - // Filler for the 1st layer interface, if different from filler_interface. - auto filler_raft_contact_ptr = std::unique_ptr(range.begin() == n_raft_layers && config.support_material_interface_layers.value == 0 ? - Fill::new_from_type(support_params.raft_interface_fill_pattern) : nullptr); - // Pointer to the 1st layer interface filler. - auto filler_raft_contact = filler_raft_contact_ptr ? filler_raft_contact_ptr.get() : filler_interface.get(); - // Filler for the base interface (to be used for soluble interface / non soluble base, to produce non soluble interface layer below soluble interface layer). - auto filler_base_interface = std::unique_ptr(base_interface_layers.empty() ? nullptr : - Fill::new_from_type(support_params.interface_density > 0.95 || support_params.with_sheath ? ipRectilinear : ipSupportBase)); - auto filler_support = std::unique_ptr(Fill::new_from_type(support_params.base_fill_pattern)); - filler_interface->set_bounding_box(bbox_object); - if (filler_first_layer_ptr) - filler_first_layer_ptr->set_bounding_box(bbox_object); - if (filler_raft_contact_ptr) - filler_raft_contact_ptr->set_bounding_box(bbox_object); - if (filler_base_interface) - filler_base_interface->set_bounding_box(bbox_object); - filler_support->set_bounding_box(bbox_object); - for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) - { - SupportLayer &support_layer = *support_layers[support_layer_id]; - LayerCache &layer_cache = layer_caches[support_layer_id]; - const float support_interface_angle = config.support_material_style.value == smsGrid ? - support_params.interface_angle : support_params.raft_interface_angle(support_layer.interface_id()); - - // Find polygons with the same print_z. - SupportGeneratorLayerExtruded &bottom_contact_layer = layer_cache.bottom_contact_layer; - SupportGeneratorLayerExtruded &top_contact_layer = layer_cache.top_contact_layer; - SupportGeneratorLayerExtruded &base_layer = layer_cache.base_layer; - SupportGeneratorLayerExtruded &interface_layer = layer_cache.interface_layer; - SupportGeneratorLayerExtruded &base_interface_layer = layer_cache.base_interface_layer; - // Increment the layer indices to find a layer at support_layer.print_z. - { - auto fun = [&support_layer](const SupportGeneratorLayer *l){ return l->print_z >= support_layer.print_z - EPSILON; }; - idx_layer_bottom_contact = idx_higher_or_equal(bottom_contacts, idx_layer_bottom_contact, fun); - idx_layer_top_contact = idx_higher_or_equal(top_contacts, idx_layer_top_contact, fun); - idx_layer_intermediate = idx_higher_or_equal(intermediate_layers, idx_layer_intermediate, fun); - idx_layer_interface = idx_higher_or_equal(interface_layers, idx_layer_interface, fun); - idx_layer_base_interface = idx_higher_or_equal(base_interface_layers, idx_layer_base_interface,fun); - } - // Copy polygons from the layers. - if (idx_layer_bottom_contact < bottom_contacts.size() && bottom_contacts[idx_layer_bottom_contact]->print_z < support_layer.print_z + EPSILON) - bottom_contact_layer.layer = bottom_contacts[idx_layer_bottom_contact]; - if (idx_layer_top_contact < top_contacts.size() && top_contacts[idx_layer_top_contact]->print_z < support_layer.print_z + EPSILON) - top_contact_layer.layer = top_contacts[idx_layer_top_contact]; - if (idx_layer_interface < interface_layers.size() && interface_layers[idx_layer_interface]->print_z < support_layer.print_z + EPSILON) - interface_layer.layer = interface_layers[idx_layer_interface]; - if (idx_layer_base_interface < base_interface_layers.size() && base_interface_layers[idx_layer_base_interface]->print_z < support_layer.print_z + EPSILON) - base_interface_layer.layer = base_interface_layers[idx_layer_base_interface]; - if (idx_layer_intermediate < intermediate_layers.size() && intermediate_layers[idx_layer_intermediate]->print_z < support_layer.print_z + EPSILON) - base_layer.layer = intermediate_layers[idx_layer_intermediate]; - - // This layer is a raft contact layer. Any contact polygons at this layer are raft contacts. - bool raft_layer = slicing_params.interface_raft_layers && top_contact_layer.layer && is_approx(top_contact_layer.layer->print_z, slicing_params.raft_contact_top_z); - if (config.support_material_interface_layers == 0) { - // If no top interface layers were requested, we treat the contact layer exactly as a generic base layer. - // Don't merge the raft contact layer though. - if (support_params.can_merge_support_regions && ! raft_layer) { - if (base_layer.could_merge(top_contact_layer)) - base_layer.merge(std::move(top_contact_layer)); - else if (base_layer.empty()) - base_layer = std::move(top_contact_layer); - } - } else { - loop_interface_processor.generate(top_contact_layer, support_params.support_material_interface_flow); - // If no loops are allowed, we treat the contact layer exactly as a generic interface layer. - // Merge interface_layer into top_contact_layer, as the top_contact_layer is not synchronized and therefore it will be used - // to trim other layers. - if (top_contact_layer.could_merge(interface_layer) && ! raft_layer) - top_contact_layer.merge(std::move(interface_layer)); - } - if ((config.support_material_interface_layers == 0 || config.support_material_bottom_interface_layers == 0) && support_params.can_merge_support_regions) { - if (base_layer.could_merge(bottom_contact_layer)) - base_layer.merge(std::move(bottom_contact_layer)); - else if (base_layer.empty() && ! bottom_contact_layer.empty() && ! bottom_contact_layer.layer->bridging) - base_layer = std::move(bottom_contact_layer); - } else if (bottom_contact_layer.could_merge(top_contact_layer) && ! raft_layer) - top_contact_layer.merge(std::move(bottom_contact_layer)); - else if (bottom_contact_layer.could_merge(interface_layer)) - bottom_contact_layer.merge(std::move(interface_layer)); - -#if 0 - if ( ! interface_layer.empty() && ! base_layer.empty()) { - // turn base support into interface when it's contained in our holes - // (this way we get wider interface anchoring) - //FIXME The intention of the code below is unclear. One likely wanted to just merge small islands of base layers filling in the holes - // inside interface layers, but the code below fills just too much, see GH #4570 - Polygons islands = top_level_islands(interface_layer.layer->polygons); - polygons_append(interface_layer.layer->polygons, intersection(base_layer.layer->polygons, islands)); - base_layer.layer->polygons = diff(base_layer.layer->polygons, islands); - } -#endif - - // Top and bottom contacts, interface layers. - enum class InterfaceLayerType { TopContact, BottomContact, RaftContact, Interface, InterfaceAsBase }; - auto extrude_interface = [&](SupportGeneratorLayerExtruded &layer_ex, InterfaceLayerType interface_layer_type) { - if (! layer_ex.empty() && ! layer_ex.polygons_to_extrude().empty()) { - bool interface_as_base = interface_layer_type == InterfaceLayerType::InterfaceAsBase; - bool raft_contact = interface_layer_type == InterfaceLayerType::RaftContact; - //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore - // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) - auto *filler = raft_contact ? filler_raft_contact : filler_interface.get(); - auto interface_flow = layer_ex.layer->bridging ? - Flow::bridging_flow(layer_ex.layer->height, support_params.support_material_bottom_interface_flow.nozzle_diameter()) : - (raft_contact ? &support_params.raft_interface_flow : - interface_as_base ? &support_params.support_material_flow : &support_params.support_material_interface_flow) - ->with_height(float(layer_ex.layer->height)); - filler->angle = interface_as_base ? - // If zero interface layers are configured, use the same angle as for the base layers. - angles[support_layer_id % angles.size()] : - // Use interface angle for the interface layers. - raft_contact ? - support_params.raft_interface_angle(support_layer.interface_id()) : - support_interface_angle; - double density = raft_contact ? support_params.raft_interface_density : interface_as_base ? support_params.support_density : support_params.interface_density; - filler->spacing = raft_contact ? support_params.raft_interface_flow.spacing() : - interface_as_base ? support_params.support_material_flow.spacing() : support_params.support_material_interface_flow.spacing(); - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); - fill_expolygons_generate_paths( - // Destination - layer_ex.extrusions, - // Regions to fill - union_safety_offset_ex(layer_ex.polygons_to_extrude()), - // Filler and its parameters - filler, float(density), - // Extrusion parameters - ExtrusionRole::SupportMaterialInterface, interface_flow); - } - }; - const bool top_interfaces = config.support_material_interface_layers.value != 0; - const bool bottom_interfaces = top_interfaces && config.support_material_bottom_interface_layers != 0; - extrude_interface(top_contact_layer, raft_layer ? InterfaceLayerType::RaftContact : top_interfaces ? InterfaceLayerType::TopContact : InterfaceLayerType::InterfaceAsBase); - extrude_interface(bottom_contact_layer, bottom_interfaces ? InterfaceLayerType::BottomContact : InterfaceLayerType::InterfaceAsBase); - extrude_interface(interface_layer, top_interfaces ? InterfaceLayerType::Interface : InterfaceLayerType::InterfaceAsBase); - - // Base interface layers under soluble interfaces - if ( ! base_interface_layer.empty() && ! base_interface_layer.polygons_to_extrude().empty()) { - Fill *filler = filler_base_interface.get(); - //FIXME Bottom interfaces are extruded with the briding flow. Some bridging layers have its height slightly reduced, therefore - // the bridging flow does not quite apply. Reduce the flow to area of an ellipse? (A = pi * a * b) - assert(! base_interface_layer.layer->bridging); - Flow interface_flow = support_params.support_material_flow.with_height(float(base_interface_layer.layer->height)); - filler->angle = support_interface_angle; - filler->spacing = support_params.support_material_interface_flow.spacing(); - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.interface_density)); - fill_expolygons_generate_paths( - // Destination - base_interface_layer.extrusions, - //base_layer_interface.extrusions, - // Regions to fill - union_safety_offset_ex(base_interface_layer.polygons_to_extrude()), - // Filler and its parameters - filler, float(support_params.interface_density), - // Extrusion parameters - ExtrusionRole::SupportMaterial, interface_flow); - } - - // Base support or flange. - if (! base_layer.empty() && ! base_layer.polygons_to_extrude().empty()) { - Fill *filler = filler_support.get(); - filler->angle = angles[support_layer_id % angles.size()]; - // We don't use $base_flow->spacing because we need a constant spacing - // value that guarantees that all layers are correctly aligned. - assert(! base_layer.layer->bridging); - auto flow = support_params.support_material_flow.with_height(float(base_layer.layer->height)); - filler->spacing = support_params.support_material_flow.spacing(); - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / support_params.support_density)); - float density = float(support_params.support_density); - bool sheath = support_params.with_sheath; - bool no_sort = false; - bool done = false; - if (base_layer.layer->bottom_z < EPSILON) { - // Base flange (the 1st layer). - filler = filler_first_layer; - filler->angle = Geometry::deg2rad(float(config.support_material_angle.value + 90.)); - density = float(config.raft_first_layer_density.value * 0.01); - flow = support_params.first_layer_flow; - // use the proper spacing for first layer as we don't need to align - // its pattern to the other layers - //FIXME When paralellizing, each thread shall have its own copy of the fillers. - filler->spacing = flow.spacing(); - filler->link_max_length = coord_t(scale_(filler->spacing * link_max_length_factor / density)); - sheath = true; - no_sort = true; - } else if (config.support_material_style == SupportMaterialStyle::smsOrganic) { - tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow); - done = true; - } - if (! done) - fill_expolygons_with_sheath_generate_paths( - // Destination - base_layer.extrusions, - // Regions to fill - base_layer.polygons_to_extrude(), - // Filler and its parameters - filler, density, - // Extrusion parameters - ExtrusionRole::SupportMaterial, flow, - sheath, no_sort); - } - - // Merge base_interface_layers to base_layers to avoid unneccessary retractions - if (! base_layer.empty() && ! base_interface_layer.empty() && ! base_layer.polygons_to_extrude().empty() && ! base_interface_layer.polygons_to_extrude().empty() && - base_layer.could_merge(base_interface_layer)) - base_layer.merge(std::move(base_interface_layer)); - - layer_cache.add_nonempty_and_sort(); - - // Collect the support areas with this print_z into islands, as there is no need - // for retraction over these islands. - Polygons polys; - // Collect the extrusions, sorted by the bottom extrusion height. - for (LayerCacheItem &layer_cache_item : layer_cache.nonempty) { - // Collect islands to polys. - layer_cache_item.layer_extruded->polygons_append(polys); - // The print_z of the top contact surfaces and bottom_z of the bottom contact surfaces are "free" - // in a sense that they are not synchronized with other support layers. As the top and bottom contact surfaces - // are inflated to achieve a better anchoring, it may happen, that these surfaces will at least partially - // overlap in Z with another support layers, leading to over-extrusion. - // Mitigate the over-extrusion by modulating the extrusion rate over these regions. - // The print head will follow the same print_z, but the layer thickness will be reduced - // where it overlaps with another support layer. - //FIXME When printing a briging path, what is an equivalent height of the squished extrudate of the same width? - // Collect overlapping top/bottom surfaces. - layer_cache_item.overlapping.reserve(20); - coordf_t bottom_z = layer_cache_item.layer_extruded->layer->bottom_print_z() + EPSILON; - auto add_overlapping = [&layer_cache_item, bottom_z](const SupportGeneratorLayersPtr &layers, size_t idx_top) { - for (int i = int(idx_top) - 1; i >= 0 && layers[i]->print_z > bottom_z; -- i) - layer_cache_item.overlapping.push_back(layers[i]); - }; - add_overlapping(top_contacts, idx_layer_top_contact); - if (layer_cache_item.layer_extruded->layer->layer_type == SupporLayerType::BottomContact) { - // Bottom contact layer may overlap with a base layer, which may be changed to interface layer. - add_overlapping(intermediate_layers, idx_layer_intermediate); - add_overlapping(interface_layers, idx_layer_interface); - add_overlapping(base_interface_layers, idx_layer_base_interface); - } - // Order the layers by lexicographically by an increasing print_z and a decreasing layer height. - std::stable_sort(layer_cache_item.overlapping.begin(), layer_cache_item.overlapping.end(), [](auto *l1, auto *l2) { return *l1 < *l2; }); - } - assert(support_layer.support_islands.empty()); - if (! polys.empty()) { - support_layer.support_islands = union_ex(polys); - support_layer.support_islands_bboxes.reserve(support_layer.support_islands.size()); - for (const ExPolygon &expoly : support_layer.support_islands) - support_layer.support_islands_bboxes.emplace_back(get_extents(expoly).inflated(SCALED_EPSILON)); - } - } // for each support_layer_id - }); - - // Now modulate the support layer height in parallel. - tbb::parallel_for(tbb::blocked_range(n_raft_layers, support_layers.size()), - [&support_layers, &layer_caches] - (const tbb::blocked_range& range) { - for (size_t support_layer_id = range.begin(); support_layer_id < range.end(); ++ support_layer_id) { - SupportLayer &support_layer = *support_layers[support_layer_id]; - LayerCache &layer_cache = layer_caches[support_layer_id]; - // For all extrusion types at this print_z, ordered by decreasing layer height: - for (LayerCacheItem &layer_cache_item : layer_cache.nonempty) { - // Trim the extrusion height from the bottom by the overlapping layers. - modulate_extrusion_by_overlapping_layers(layer_cache_item.layer_extruded->extrusions, *layer_cache_item.layer_extruded->layer, layer_cache_item.overlapping); - support_layer.support_fills.append(std::move(layer_cache_item.layer_extruded->extrusions)); - } - } - }); - -#ifndef NDEBUG - struct Test { - static bool verify_nonempty(const ExtrusionEntityCollection *collection) { - for (const ExtrusionEntity *ee : collection->entities) { - if (const ExtrusionPath *path = dynamic_cast(ee)) - assert(! path->empty()); - else if (const ExtrusionMultiPath *multipath = dynamic_cast(ee)) - assert(! multipath->empty()); - else if (const ExtrusionEntityCollection *eecol = dynamic_cast(ee)) { - assert(! eecol->empty()); - return verify_nonempty(eecol); - } else - assert(false); - } - return true; - } - }; - for (const SupportLayer *support_layer : support_layers) - assert(Test::verify_nonempty(&support_layer->support_fills)); -#endif // NDEBUG -} - /* void PrintObjectSupportMaterial::clip_by_pillars( const PrintObject &object, diff --git a/src/libslic3r/SupportMaterial.hpp b/src/libslic3r/SupportMaterial.hpp index 24dc8507e..bbb991c3e 100644 --- a/src/libslic3r/SupportMaterial.hpp +++ b/src/libslic3r/SupportMaterial.hpp @@ -12,54 +12,6 @@ namespace Slic3r { class PrintObject; -// Remove bridges from support contact areas. -// To be called if PrintObjectConfig::dont_support_bridges. -void remove_bridges_from_contacts( - const PrintConfig &print_config, - const Layer &lower_layer, - const LayerRegion &layerm, - float fw, - Polygons &contact_polygons); - -// Generate raft layers, also expand the 1st support layer -// in case there is no raft layer to improve support adhesion. -SupportGeneratorLayersPtr generate_raft_base( - const PrintObject &object, - const SupportParameters &support_params, - const SlicingParameters &slicing_params, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers, - const SupportGeneratorLayersPtr &base_layers, - SupportGeneratorLayerStorage &layer_storage); - -// returns sorted layers -SupportGeneratorLayersPtr generate_support_layers( - PrintObject &object, - const SupportGeneratorLayersPtr &raft_layers, - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &intermediate_layers, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers); - -// Produce the support G-code. -// Used by both classic and tree supports. -void generate_support_toolpaths( - SupportLayerPtrs &support_layers, - const PrintObjectConfig &config, - const SupportParameters &support_params, - const SlicingParameters &slicing_params, - const SupportGeneratorLayersPtr &raft_layers, - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - const SupportGeneratorLayersPtr &intermediate_layers, - const SupportGeneratorLayersPtr &interface_layers, - const SupportGeneratorLayersPtr &base_interface_layers); - -void export_print_z_polygons_to_svg(const char *path, SupportGeneratorLayer ** const layers, size_t n_layers); -void export_print_z_polygons_and_extrusions_to_svg(const char *path, SupportGeneratorLayer ** const layers, size_t n_layers, SupportLayer& support_layer); - // This class manages raft and supports for a single PrintObject. // Instantiated by Slic3r::Print::Object->_support_material() // This class is instantiated before the slicing starts as Object.pm will query @@ -84,6 +36,10 @@ public: void generate(PrintObject &object); private: + using SupportGeneratorLayersPtr = FFFSupport::SupportGeneratorLayersPtr; + using SupportGeneratorLayerStorage = FFFSupport::SupportGeneratorLayerStorage; + using SupportParameters = FFFSupport::SupportParameters; + std::vector buildplate_covered(const PrintObject &object) const; // Generate top contact layers supporting overhangs. diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/TreeSupport.cpp index 9a7b203c6..a3264990a 100644 --- a/src/libslic3r/TreeSupport.cpp +++ b/src/libslic3r/TreeSupport.cpp @@ -19,9 +19,10 @@ #include "Polygon.hpp" #include "Polyline.hpp" #include "MutablePolygon.hpp" -#include "SupportMaterial.hpp" #include "TriangleMeshSlicer.hpp" +#include "Support/SupportCommon.hpp" + #include #include #include @@ -37,7 +38,6 @@ #include #include -#include #if defined(TREE_SUPPORT_SHOW_ERRORS) && defined(_WIN32) #define TREE_SUPPORT_SHOW_ERRORS_WIN32 @@ -53,6 +53,8 @@ // #define TREESUPPORT_DEBUG_SVG +using namespace Slic3r::FFFSupport; + namespace Slic3r { @@ -962,13 +964,11 @@ static LayerIndex layer_idx_floor(const SlicingParameters &slicing_params, const } static inline SupportGeneratorLayer& layer_initialize( - SupportGeneratorLayer &layer_new, - const SupporLayerType layer_type, - const SlicingParameters &slicing_params, + SupportGeneratorLayer &layer_new, + const SlicingParameters &slicing_params, const TreeSupportSettings &config, - const size_t layer_idx) + const size_t layer_idx) { - layer_new.layer_type = layer_type; layer_new.print_z = layer_z(slicing_params, config, layer_idx); layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0; layer_new.height = layer_new.print_z - layer_new.bottom_z; @@ -976,29 +976,26 @@ static inline SupportGeneratorLayer& layer_initialize( } // Using the std::deque as an allocator. -inline SupportGeneratorLayer& layer_allocate( - std::deque &layer_storage, +inline SupportGeneratorLayer& layer_allocate_unguarded( + SupportGeneratorLayerStorage &layer_storage, SupporLayerType layer_type, const SlicingParameters &slicing_params, const TreeSupportSettings &config, size_t layer_idx) { - //FIXME take raft into account. - layer_storage.push_back(SupportGeneratorLayer()); - return layer_initialize(layer_storage.back(), layer_type, slicing_params, config, layer_idx); + SupportGeneratorLayer &layer = layer_storage.allocate_unguarded(layer_type); + return layer_initialize(layer, slicing_params, config, layer_idx); } inline SupportGeneratorLayer& layer_allocate( - std::deque &layer_storage, - tbb::spin_mutex& layer_storage_mutex, + SupportGeneratorLayerStorage &layer_storage, SupporLayerType layer_type, const SlicingParameters &slicing_params, const TreeSupportSettings &config, size_t layer_idx) { - tbb::spin_mutex::scoped_lock lock(layer_storage_mutex); - layer_storage.push_back(SupportGeneratorLayer()); - return layer_initialize(layer_storage.back(), layer_type, slicing_params, config, layer_idx); + SupportGeneratorLayer &layer = layer_storage.allocate(layer_type); + return layer_initialize(layer, slicing_params, config, layer_idx); } int generate_raft_contact( @@ -1016,7 +1013,7 @@ int generate_raft_contact( while (raft_contact_layer_idx > 0 && config.raft_layers[raft_contact_layer_idx] > print_object.slicing_parameters().raft_contact_top_z + EPSILON) -- raft_contact_layer_idx; // Create the raft contact layer. - SupportGeneratorLayer &raft_contact_layer = layer_allocate(layer_storage, SupporLayerType::TopContact, print_object.slicing_parameters(), config, raft_contact_layer_idx); + SupportGeneratorLayer &raft_contact_layer = layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, print_object.slicing_parameters(), config, raft_contact_layer_idx); top_contacts[raft_contact_layer_idx] = &raft_contact_layer; const ExPolygons &lslices = print_object.get_layer(0)->lslices; double expansion = print_object.config().raft_expansion.value; @@ -1115,7 +1112,7 @@ public: { SupportGeneratorLayer*& l = top_contacts[insert_layer_idx]; if (l == nullptr) - l = &layer_allocate(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, insert_layer_idx); + l = &layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, insert_layer_idx); // will be unioned in finalize_interface_and_support_areas() append(l->polygons, std::move(new_roofs)); } @@ -1141,7 +1138,7 @@ public: std::lock_guard lock(m_mutex_layer_storage); SupportGeneratorLayer*& l = top_contacts[0]; if (l == nullptr) - l = &layer_allocate(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, 0); + l = &layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, 0); append(l->polygons, std::move(overhang_areas)); } @@ -3309,7 +3306,6 @@ static void finalize_interface_and_support_areas( #endif // SLIC3R_TREESUPPORTS_PROGRESS // Iterate over the generated circles in parallel and clean them up. Also add support floor. - tbb::spin_mutex layer_storage_mutex; tbb::parallel_for(tbb::blocked_range(0, support_layer_storage.size()), [&](const tbb::blocked_range &range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { @@ -3402,7 +3398,7 @@ static void finalize_interface_and_support_areas( } if (! floor_layer.empty()) { if (support_bottom == nullptr) - support_bottom = &layer_allocate(layer_storage, layer_storage_mutex, SupporLayerType::BottomContact, print_object.slicing_parameters(), config, layer_idx); + support_bottom = &layer_allocate(layer_storage, SupporLayerType::BottomContact, print_object.slicing_parameters(), config, layer_idx); support_bottom->polygons = union_(floor_layer, support_bottom->polygons); base_layer_polygons = diff_clipped(base_layer_polygons, offset(support_bottom->polygons, scaled(0.01), jtMiter, 1.2)); // Subtract the support floor from the normal support. } @@ -3410,11 +3406,11 @@ static void finalize_interface_and_support_areas( if (! support_roof_polygons.empty()) { if (support_roof == nullptr) - support_roof = top_contacts[layer_idx] = &layer_allocate(layer_storage, layer_storage_mutex, SupporLayerType::TopContact, print_object.slicing_parameters(), config, layer_idx); + support_roof = top_contacts[layer_idx] = &layer_allocate(layer_storage, SupporLayerType::TopContact, print_object.slicing_parameters(), config, layer_idx); support_roof->polygons = union_(support_roof_polygons); } if (! base_layer_polygons.empty()) { - SupportGeneratorLayer *base_layer = intermediate_layers[layer_idx] = &layer_allocate(layer_storage, layer_storage_mutex, SupporLayerType::Base, print_object.slicing_parameters(), config, layer_idx); + SupportGeneratorLayer *base_layer = intermediate_layers[layer_idx] = &layer_allocate(layer_storage, SupporLayerType::Base, print_object.slicing_parameters(), config, layer_idx); base_layer->polygons = union_(base_layer_polygons); } From 287304214828288ede6c76073b0ba12589b8e7c1 Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 5 May 2023 13:52:20 +0200 Subject: [PATCH 102/115] SPE-1684: Fix issue where the automatic painting did not trigger paint-on-supports notification in the right panel --- src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp index 7700e1eef..557cdee8e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoFdmSupports.cpp @@ -434,18 +434,15 @@ void GLGizmoFdmSupports::apply_data_from_backend() mesh_id++; auto selector = selectors.find(mv->id().id); if (selector != selectors.end()) { - mv->supported_facets.set(selector->second.selector); - m_triangle_selectors[mesh_id]->deserialize(mv->supported_facets.get_data(), true); + m_triangle_selectors[mesh_id]->deserialize(selector->second.selector.serialize(), true); m_triangle_selectors[mesh_id]->request_update_render_data(); } } } - - m_parent.post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS)); - m_parent.set_as_dirty(); } - this->waiting_for_autogenerated_supports = false; } + this->waiting_for_autogenerated_supports = false; + update_model_object(); } void GLGizmoFdmSupports::update_model_object() const From ad203baf77d152c25d718bf270df3d5dc38991fe Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 5 May 2023 14:05:22 +0200 Subject: [PATCH 103/115] Moved FFF support code to libslic3r/Support directory --- src/libslic3r/CMakeLists.txt | 12 +++++----- src/libslic3r/Print.cpp | 1 - src/libslic3r/PrintObject.cpp | 6 ++--- src/libslic3r/Support/SupportCommon.cpp | 4 ++-- src/libslic3r/Support/SupportDebug.cpp | 6 ++--- .../{ => Support}/SupportMaterial.cpp | 22 +++++++++---------- .../{ => Support}/SupportMaterial.hpp | 10 ++++----- .../{ => Support}/TreeModelVolumes.cpp | 18 +++++++-------- .../{ => Support}/TreeModelVolumes.hpp | 6 ++--- src/libslic3r/{ => Support}/TreeSupport.cpp | 0 src/libslic3r/{ => Support}/TreeSupport.hpp | 0 11 files changed, 42 insertions(+), 43 deletions(-) rename src/libslic3r/{ => Support}/SupportMaterial.cpp (99%) rename src/libslic3r/{ => Support}/SupportMaterial.hpp (97%) rename src/libslic3r/{ => Support}/TreeModelVolumes.cpp (99%) rename src/libslic3r/{ => Support}/TreeModelVolumes.hpp (99%) rename src/libslic3r/{ => Support}/TreeSupport.cpp (100%) rename src/libslic3r/{ => Support}/TreeSupport.hpp (100%) diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 11f7adf76..d44bfcde0 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -281,10 +281,14 @@ set(SLIC3R_SOURCES Support/SupportDebug.cpp Support/SupportDebug.hpp Support/SupportLayer.hpp + Support/SupportMaterial.cpp + Support/SupportMaterial.hpp Support/SupportParameters.cpp Support/SupportParameters.hpp - SupportMaterial.cpp - SupportMaterial.hpp + Support/TreeSupport.cpp + Support/TreeSupport.hpp + Support/TreeModelVolumes.cpp + Support/TreeModelVolumes.hpp SupportSpotsGenerator.cpp SupportSpotsGenerator.hpp Surface.cpp @@ -298,10 +302,6 @@ set(SLIC3R_SOURCES Tesselate.cpp Tesselate.hpp TextConfiguration.hpp - TreeSupport.cpp - TreeSupport.hpp - TreeModelVolumes.cpp - TreeModelVolumes.hpp TriangleMesh.cpp TriangleMesh.hpp TriangleMeshSlicer.cpp diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index cf8b5c577..5b23dc260 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -8,7 +8,6 @@ #include "Geometry/ConvexHull.hpp" #include "I18N.hpp" #include "ShortestPath.hpp" -#include "SupportMaterial.hpp" #include "Thread.hpp" #include "GCode.hpp" #include "GCode/WipeTower.hpp" diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 46588b9b0..b43afd6be 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -17,8 +17,8 @@ #include "MutablePolygon.hpp" #include "PrintBase.hpp" #include "PrintConfig.hpp" -#include "SupportMaterial.hpp" -#include "TreeSupport.hpp" +#include "Support/SupportMaterial.hpp" +#include "Support/TreeSupport.hpp" #include "Surface.hpp" #include "Slicing.hpp" #include "Tesselate.hpp" @@ -27,7 +27,7 @@ #include "Fill/FillAdaptive.hpp" #include "Fill/FillLightning.hpp" #include "Format/STL.hpp" -#include "SupportMaterial.hpp" +#include "Support/SupportMaterial.hpp" #include "SupportSpotsGenerator.hpp" #include "TriangleSelectorWrapper.hpp" #include "format.hpp" diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 8825d8dd2..f76ebaa0e 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -22,8 +22,8 @@ #define DEBUG #define _DEBUG #undef NDEBUG - #include "utils.hpp" - #include "SVG.hpp" + #include "../utils.hpp" + #include "../SVG.hpp" #endif #include diff --git a/src/libslic3r/Support/SupportDebug.cpp b/src/libslic3r/Support/SupportDebug.cpp index 8cec806c1..5c18bc769 100644 --- a/src/libslic3r/Support/SupportDebug.cpp +++ b/src/libslic3r/Support/SupportDebug.cpp @@ -1,9 +1,9 @@ #if 1 //#ifdef SLIC3R_DEBUG -#include "ClipperUtils.hpp" -#include "SVG.hpp" - +#include "../ClipperUtils.hpp" +#include "../SVG.hpp" #include "../Layer.hpp" + #include "SupportLayer.hpp" namespace Slic3r::FFFSupport { diff --git a/src/libslic3r/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp similarity index 99% rename from src/libslic3r/SupportMaterial.cpp rename to src/libslic3r/Support/SupportMaterial.cpp index 40221ec2a..347278911 100644 --- a/src/libslic3r/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -1,14 +1,14 @@ -#include "ClipperUtils.hpp" -#include "ExtrusionEntityCollection.hpp" -#include "Layer.hpp" -#include "Print.hpp" -#include "SupportMaterial.hpp" -#include "Fill/FillBase.hpp" -#include "Geometry.hpp" -#include "Point.hpp" -#include "MutablePolygon.hpp" +#include "../ClipperUtils.hpp" +#include "../ExtrusionEntityCollection.hpp" +#include "../Layer.hpp" +#include "../Print.hpp" +#include "../Fill/FillBase.hpp" +#include "../Geometry.hpp" +#include "../Point.hpp" +#include "../MutablePolygon.hpp" #include "Support/SupportCommon.hpp" +#include "SupportMaterial.hpp" #include @@ -40,8 +40,8 @@ #define DEBUG #define _DEBUG #undef NDEBUG - #include "utils.hpp" - #include "SVG.hpp" + #include "../utils.hpp" + #include "../SVG.hpp" #endif #include diff --git a/src/libslic3r/SupportMaterial.hpp b/src/libslic3r/Support/SupportMaterial.hpp similarity index 97% rename from src/libslic3r/SupportMaterial.hpp rename to src/libslic3r/Support/SupportMaterial.hpp index bbb991c3e..924ed1a05 100644 --- a/src/libslic3r/SupportMaterial.hpp +++ b/src/libslic3r/Support/SupportMaterial.hpp @@ -1,12 +1,12 @@ #ifndef slic3r_SupportMaterial_hpp_ #define slic3r_SupportMaterial_hpp_ -#include "Flow.hpp" -#include "PrintConfig.hpp" -#include "Slicing.hpp" +#include "../Flow.hpp" +#include "../PrintConfig.hpp" +#include "../Slicing.hpp" -#include "Support/SupportLayer.hpp" -#include "Support/SupportParameters.hpp" +#include "SupportLayer.hpp" +#include "SupportParameters.hpp" namespace Slic3r { diff --git a/src/libslic3r/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp similarity index 99% rename from src/libslic3r/TreeModelVolumes.cpp rename to src/libslic3r/Support/TreeModelVolumes.cpp index 96f61a07d..fd5437f9e 100644 --- a/src/libslic3r/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -9,15 +9,15 @@ #include "TreeModelVolumes.hpp" #include "TreeSupport.hpp" -#include "BuildVolume.hpp" -#include "ClipperUtils.hpp" -#include "Flow.hpp" -#include "Layer.hpp" -#include "Point.hpp" -#include "Print.hpp" -#include "PrintConfig.hpp" -#include "Utils.hpp" -#include "format.hpp" +#include "../BuildVolume.hpp" +#include "../ClipperUtils.hpp" +#include "../Flow.hpp" +#include "../Layer.hpp" +#include "../Point.hpp" +#include "../Print.hpp" +#include "../PrintConfig.hpp" +#include "../Utils.hpp" +#include "../format.hpp" #include diff --git a/src/libslic3r/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp similarity index 99% rename from src/libslic3r/TreeModelVolumes.hpp rename to src/libslic3r/Support/TreeModelVolumes.hpp index 659baf1ff..2b7ab5e1b 100644 --- a/src/libslic3r/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -14,9 +14,9 @@ #include -#include "Point.hpp" -#include "Polygon.hpp" -#include "PrintConfig.hpp" +#include "../Point.hpp" +#include "../Polygon.hpp" +#include "../PrintConfig.hpp" namespace Slic3r { diff --git a/src/libslic3r/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp similarity index 100% rename from src/libslic3r/TreeSupport.cpp rename to src/libslic3r/Support/TreeSupport.cpp diff --git a/src/libslic3r/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp similarity index 100% rename from src/libslic3r/TreeSupport.hpp rename to src/libslic3r/Support/TreeSupport.hpp From 89f0895dd67f6ec94732ca98c4e0f69dd1645870 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Fri, 5 May 2023 14:23:09 +0200 Subject: [PATCH 104/115] WIP: Organic supports - bottom interfaces --- src/libslic3r/Support/TreeModelVolumes.cpp | 4 +++- src/libslic3r/Support/TreeModelVolumes.hpp | 4 +++- src/libslic3r/Support/TreeSupport.cpp | 18 +++++++++++++----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index fd5437f9e..5df56cd62 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -79,7 +79,9 @@ TreeSupportMeshGroupSettings::TreeSupportMeshGroupSettings(const PrintObject &pr // this->support_interface_skip_height = // this->support_infill_angles = this->support_roof_enable = config.support_material_interface_layers.value > 0; - this->support_roof_height = config.support_material_interface_layers.value * this->layer_height; + this->support_roof_layers = this->support_roof_enable ? config.support_material_interface_layers.value : 0; + this->support_floor_enable = config.support_material_interface_layers.value > 0 && config.support_material_bottom_interface_layers.value > 0; + this->support_floor_layers = this->support_floor_enable ? config.support_material_bottom_interface_layers.value : 0; // this->minimum_roof_area = // this->support_roof_angles = this->support_roof_pattern = config.support_material_interface_pattern; diff --git a/src/libslic3r/Support/TreeModelVolumes.hpp b/src/libslic3r/Support/TreeModelVolumes.hpp index 2b7ab5e1b..ecfa99971 100644 --- a/src/libslic3r/Support/TreeModelVolumes.hpp +++ b/src/libslic3r/Support/TreeModelVolumes.hpp @@ -94,7 +94,9 @@ struct TreeSupportMeshGroupSettings { bool support_roof_enable { false }; // Support Roof Thickness // The thickness of the support roofs. This controls the amount of dense layers at the top of the support on which the model rests. - coord_t support_roof_height { scaled(1.) }; + coord_t support_roof_layers { 2 }; + bool support_floor_enable { false }; + coord_t support_floor_layers { 2 }; // Minimum Support Roof Area // Minimum area size for the roofs of the support. Polygons which have an area smaller than this value will be printed as normal support. double minimum_roof_area { scaled(scaled(1.)) }; diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index a3264990a..d1306962e 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1460,7 +1460,7 @@ static void generate_initial_areas( //FIXME this is a heuristic value for support enforcers to work. // + 10 * config.support_line_width; ; - const size_t num_support_roof_layers = mesh_group_settings.support_roof_enable ? (mesh_group_settings.support_roof_height + config.layer_height / 2) / config.layer_height : 0; + const size_t num_support_roof_layers = mesh_group_settings.support_roof_layers; const bool roof_enabled = num_support_roof_layers > 0; const bool force_tip_to_roof = sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area && roof_enabled; // cap for how much layer below the overhang a new support point may be added, as other than with regular support every new inserted point @@ -4211,6 +4211,7 @@ static std::vector draw_branches( struct Slice { Polygons polygons; + Polygons bottom_interfaces; size_t num_branches{ 0 }; }; @@ -4310,6 +4311,7 @@ static std::vector draw_branches( const SlicingParameters &slicing_params = print_object.slicing_parameters(); MeshSlicingParams mesh_slicing_params; mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; + tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; @@ -4343,6 +4345,7 @@ static std::vector draw_branches( struct BottomExtraSlice { Polygons polygons; Polygons supported; + Polygons bottom_interfaces; double area; double supported_area; }; @@ -4371,12 +4374,17 @@ static std::vector draw_branches( */ Polygons supported; double supported_area; - bottom_extra_slices.push_back({ rest_support, std::move(supported), rest_support_area, supported_area }); + bottom_extra_slices.push_back({ rest_support, std::move(supported), {}, rest_support_area, supported_area }); } // Now remove those bottom slices that are not supported at all. - while (! bottom_extra_slices.empty() && - area(intersection_clipped(bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {}))) < support_area_min) - bottom_extra_slices.pop_back(); + while (! bottom_extra_slices.empty()) { + Polygons bottom_interfaces = intersection_clipped(bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {})); + if (area(bottom_interfaces) < support_area_min) + bottom_extra_slices.pop_back(); + else { + bottom_extra_slices.back().bottom_interfaces = std::move(bottom_interfaces); + } + } layer_begin -= LayerIndex(bottom_extra_slices.size()); slices.insert(slices.begin(), bottom_extra_slices.size(), {}); size_t i = 0; From ce4cf95067d0bd51824d3951f51881645ab2605b Mon Sep 17 00:00:00 2001 From: PavelMikus Date: Fri, 5 May 2023 16:31:27 +0200 Subject: [PATCH 105/115] Fix missing includes --- src/libslic3r/Support/SupportCommon.cpp | 1 + src/libslic3r/Support/SupportLayer.hpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index f76ebaa0e..20e8558dc 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -1,4 +1,5 @@ #include "../ClipperUtils.hpp" +#include "../ClipperZUtils.hpp" #include "../ExtrusionEntityCollection.hpp" #include "../Layer.hpp" #include "../Print.hpp" diff --git a/src/libslic3r/Support/SupportLayer.hpp b/src/libslic3r/Support/SupportLayer.hpp index 82265881c..155de70ce 100644 --- a/src/libslic3r/Support/SupportLayer.hpp +++ b/src/libslic3r/Support/SupportLayer.hpp @@ -5,6 +5,8 @@ #include // for Slic3r::deque #include "../libslic3r.h" +#include "ClipperUtils.hpp" +#include "Polygon.hpp" namespace Slic3r::FFFSupport { From 1e7a3216ca87d9fdeaa5b12f813e5900a77d49bc Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 8 May 2023 09:19:31 +0200 Subject: [PATCH 106/115] WIP Organic supports intefaces: Further refactor of FDM support code - extracted interface routine to common. Implemented support for soluble interfaces & half soluble / half non-soluble interfaces. --- src/libslic3r/Support/SupportCommon.cpp | 189 +++++++++ src/libslic3r/Support/SupportCommon.hpp | 17 + src/libslic3r/Support/SupportMaterial.cpp | 169 +------- src/libslic3r/Support/SupportMaterial.hpp | 10 - src/libslic3r/Support/SupportParameters.cpp | 28 +- src/libslic3r/Support/SupportParameters.hpp | 24 ++ src/libslic3r/Support/TreeSupport.cpp | 405 ++++++++++++-------- src/libslic3r/Support/TreeSupport.hpp | 4 - 8 files changed, 507 insertions(+), 339 deletions(-) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index f76ebaa0e..4978b9e40 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -3,6 +3,7 @@ #include "../Layer.hpp" #include "../Print.hpp" #include "../Fill/FillBase.hpp" +#include "../MutablePolygon.hpp" #include "../Geometry.hpp" #include "../Point.hpp" @@ -118,6 +119,194 @@ void remove_bridges_from_contacts( #endif /* SLIC3R_DEBUG */ } +// Convert some of the intermediate layers into top/bottom interface layers as well as base interface layers. +std::pair generate_interface_layers( + const PrintObjectConfig &config, + const SupportParameters &support_params, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + // Input / output, will be merged with output. Only provided for Organic supports. + SupportGeneratorLayersPtr &top_interface_layers, + SupportGeneratorLayersPtr &top_base_interface_layers, + // Input, will be trimmed with the newly created interface layers. + SupportGeneratorLayersPtr &intermediate_layers, + SupportGeneratorLayerStorage &layer_storage) +{ + std::pair base_and_interface_layers; + + if (! intermediate_layers.empty() && support_params.has_interfaces()) { + // For all intermediate layers, collect top contact surfaces, which are not further than support_material_interface_layers. + BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - start"; + const bool snug_supports = config.support_material_style.value != smsGrid; + SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first; + SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second; + interface_layers.assign(intermediate_layers.size(), nullptr); + if (support_params.has_base_interfaces()) + base_interface_layers.assign(intermediate_layers.size(), nullptr); + const auto smoothing_distance = support_params.support_material_interface_flow.scaled_spacing() * 1.5; + const auto minimum_island_radius = support_params.support_material_interface_flow.scaled_spacing() / support_params.interface_density; + const auto closing_distance = smoothing_distance; // scaled(config.support_material_closing_radius.value); + // Insert a new layer into base_interface_layers, if intersection with base exists. + auto insert_layer = [&layer_storage, snug_supports, closing_distance, smoothing_distance, minimum_island_radius]( + SupportGeneratorLayer &intermediate_layer, Polygons &bottom, Polygons &&top, SupportGeneratorLayer *top_interface_layer, + const Polygons *subtract, SupporLayerType type) -> SupportGeneratorLayer* { + bool has_top_interface = top_interface_layer && ! top_interface_layer->polygons.empty(); + assert(! bottom.empty() || ! top.empty() || has_top_interface); + // Merge top into bottom, unite them with a safety offset. + append(bottom, std::move(top)); + // Merge top / bottom interfaces. For snug supports, merge using closing distance and regularize (close concave corners). + bottom = intersection( + snug_supports ? + smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : + union_safety_offset(std::move(bottom)), + intermediate_layer.polygons); + if (has_top_interface) + // Don't trim the precomputed Organic supports top interface with base layer + // as the precomputed top interface likely expands over multiple tree tips. + bottom = union_(std::move(top_interface_layer->polygons), bottom); + if (! bottom.empty()) { + //FIXME Remove non-printable tiny islands, let them be printed using the base support. + //bottom = opening(std::move(bottom), minimum_island_radius); + if (! bottom.empty()) { + SupportGeneratorLayer &layer_new = top_interface_layer ? *top_interface_layer : layer_storage.allocate(type); + layer_new.polygons = std::move(bottom); + layer_new.print_z = intermediate_layer.print_z; + layer_new.bottom_z = intermediate_layer.bottom_z; + layer_new.height = intermediate_layer.height; + layer_new.bridging = intermediate_layer.bridging; + // Subtract the interface from the base regions. + intermediate_layer.polygons = diff(intermediate_layer.polygons, layer_new.polygons); + if (subtract) + // Trim the base interface layer with the interface layer. + layer_new.polygons = diff(std::move(layer_new.polygons), *subtract); + //FIXME filter layer_new.polygons islands by a minimum area? + // $interface_area = [ grep abs($_->area) >= $area_threshold, @$interface_area ]; + return &layer_new; + } + } + return nullptr; + }; + tbb::parallel_for(tbb::blocked_range(0, int(intermediate_layers.size())), + [&bottom_contacts, &top_contacts, &top_interface_layers, &top_base_interface_layers, &intermediate_layers, &insert_layer, &support_params, + snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range& range) { + // Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below + // this intermediate layer. + // Index of the first top contact layer intersecting the current intermediate layer. + auto idx_top_contact_first = -1; + // Index of the first bottom contact layer intersecting the current intermediate layer. + auto idx_bottom_contact_first = -1; + // Index of the first top interface layer intersecting the current intermediate layer. + auto idx_top_interface_first = -1; + // Index of the first top contact interface layer intersecting the current intermediate layer. + auto idx_top_base_interface_first = -1; + auto num_intermediate = int(intermediate_layers.size()); + for (int idx_intermediate_layer = range.begin(); idx_intermediate_layer < range.end(); ++ idx_intermediate_layer) { + SupportGeneratorLayer &intermediate_layer = *intermediate_layers[idx_intermediate_layer]; + Polygons polygons_top_contact_projected_interface; + Polygons polygons_top_contact_projected_base; + Polygons polygons_bottom_contact_projected_interface; + Polygons polygons_bottom_contact_projected_base; + if (support_params.num_top_interface_layers > 0) { + // Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces + coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers) - 1)]->print_z; + coordf_t top_inteface_z = std::numeric_limits::max(); + if (support_params.num_top_base_interface_layers > 0) + // Some top base interface layers will be generated. + top_inteface_z = support_params.num_top_interface_layers_only() == 0 ? + // Only base interface layers to generate. + - std::numeric_limits::max() : + intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + int(support_params.num_top_interface_layers_only()) - 1)]->print_z; + // Move idx_top_contact_first up until above the current print_z. + idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON + // Collect the top contact areas above this intermediate layer, below top_z. + for (int idx_top_contact = idx_top_contact_first; idx_top_contact < int(top_contacts.size()); ++ idx_top_contact) { + const SupportGeneratorLayer &top_contact_layer = *top_contacts[idx_top_contact]; + //FIXME maybe this adds one interface layer in excess? + if (top_contact_layer.bottom_z - EPSILON > top_z) + break; + polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface, + // For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers. + // For grid supports, merging of support regions will be performed by the projection into grid. + snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons); + } + } + if (support_params.num_bottom_interface_layers > 0) { + // Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces + coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers) + 1)]->bottom_z; + coordf_t bottom_interface_z = - std::numeric_limits::max(); + if (support_params.num_bottom_base_interface_layers > 0) + // Some bottom base interface layers will be generated. + bottom_interface_z = support_params.num_bottom_interface_layers_only() == 0 ? + // Only base interface layers to generate. + std::numeric_limits::max() : + intermediate_layers[std::max(0, idx_intermediate_layer - int(support_params.num_bottom_interface_layers_only()))]->bottom_z; + // Move idx_bottom_contact_first up until touching bottom_z. + idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; }); + // Collect the top contact areas above this intermediate layer, below top_z. + for (int idx_bottom_contact = idx_bottom_contact_first; idx_bottom_contact < int(bottom_contacts.size()); ++ idx_bottom_contact) { + const SupportGeneratorLayer &bottom_contact_layer = *bottom_contacts[idx_bottom_contact]; + if (bottom_contact_layer.print_z - EPSILON > intermediate_layer.bottom_z) + break; + polygons_append(bottom_contact_layer.print_z - EPSILON > bottom_interface_z ? polygons_bottom_contact_projected_interface : polygons_bottom_contact_projected_base, bottom_contact_layer.polygons); + } + } + auto resolve_same_layer = [](SupportGeneratorLayersPtr &layers, int &idx, coordf_t print_z) -> SupportGeneratorLayer* { + if (! layers.empty()) { + idx = idx_higher_or_equal(layers, idx, [print_z](const SupportGeneratorLayer *layer) { return layer->print_z > print_z - EPSILON; }); + if (idx < int(layers.size()) && layers[idx]->print_z < print_z + EPSILON) { + SupportGeneratorLayer *l = layers[idx]; + // Remove the layer from the source container, as it will be consumed here: It will be merged + // with the newly produced interfaces. + layers[idx] = nullptr; + return l; + } + } + return nullptr; + }; + SupportGeneratorLayer *top_interface_layer = resolve_same_layer(top_interface_layers, idx_top_interface_first, intermediate_layer.print_z); + SupportGeneratorLayer *top_base_interface_layer = resolve_same_layer(top_base_interface_layers, idx_top_base_interface_first, intermediate_layer.print_z); + SupportGeneratorLayer *interface_layer = nullptr; + if (! polygons_bottom_contact_projected_interface.empty() || ! polygons_top_contact_projected_interface.empty() || + (top_interface_layer && ! top_interface_layer->polygons.empty())) { + interface_layer = insert_layer( + intermediate_layer, polygons_bottom_contact_projected_interface, std::move(polygons_top_contact_projected_interface), top_interface_layer, + nullptr, polygons_top_contact_projected_interface.empty() ? SupporLayerType::BottomInterface : SupporLayerType::TopInterface); + interface_layers[idx_intermediate_layer] = interface_layer; + } + if (! polygons_bottom_contact_projected_base.empty() || ! polygons_top_contact_projected_base.empty() || + (top_base_interface_layer && ! top_base_interface_layer->polygons.empty())) + base_interface_layers[idx_intermediate_layer] = insert_layer( + intermediate_layer, polygons_bottom_contact_projected_base, std::move(polygons_top_contact_projected_base), top_base_interface_layer, + interface_layer ? &interface_layer->polygons : nullptr, SupporLayerType::Base); + } + }); + + // Compress contact_out, remove the nullptr items. + // The parallel_for above may not have merged all the interface and base_interface layers + // generated by the Organic supports code, do it here. + auto merge_remove_nulls = [](SupportGeneratorLayersPtr &in1, SupportGeneratorLayersPtr &in2) { + size_t nonzeros = std::count_if(in1.begin(), in1.end(), [](auto *l) { return l != nullptr; }) + + std::count_if(in2.begin(), in2.end(), [](auto *l) { return l != nullptr; }); + remove_nulls(in1); + remove_nulls(in2); + if (in2.empty()) + return std::move(in1); + else if (in1.empty()) + return std::move(in2); + else { + SupportGeneratorLayersPtr out(in1.size() + in2.size(), nullptr); + std::merge(in1.begin(), in1.end(), in2.begin(), in2.end(), out.begin(), [](auto* l, auto* r) { return l->print_z < r->print_z; }); + return std::move(out); + } + }; + interface_layers = merge_remove_nulls(interface_layers, top_interface_layers); + base_interface_layers = merge_remove_nulls(base_interface_layers, top_base_interface_layers); + BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - end"; + } + + return base_and_interface_layers; +} + SupportGeneratorLayersPtr generate_raft_base( const PrintObject &object, const SupportParameters &support_params, diff --git a/src/libslic3r/Support/SupportCommon.hpp b/src/libslic3r/Support/SupportCommon.hpp index 4eabce772..19f5822fe 100644 --- a/src/libslic3r/Support/SupportCommon.hpp +++ b/src/libslic3r/Support/SupportCommon.hpp @@ -21,6 +21,23 @@ void remove_bridges_from_contacts( float fw, Polygons &contact_polygons); +// Turn some of the base layers into base interface layers. +// For soluble interfaces with non-soluble bases, print maximum two first interface layers with the base +// extruder to improve adhesion of the soluble filament to the base. +// For Organic supports, merge top_interface_layers & top_base_interface_layers with the interfaces +// produced by this function. +std::pair generate_interface_layers( + const PrintObjectConfig &config, + const SupportParameters &support_params, + const SupportGeneratorLayersPtr &bottom_contacts, + const SupportGeneratorLayersPtr &top_contacts, + // Input / output, will be merged with output + SupportGeneratorLayersPtr &top_interface_layers, + SupportGeneratorLayersPtr &top_base_interface_layers, + // Input, will be trimmed with the newly created interface layers. + SupportGeneratorLayersPtr &intermediate_layers, + SupportGeneratorLayerStorage &layer_storage); + // Generate raft layers, also expand the 1st support layer // in case there is no raft layer to improve support adhesion. SupportGeneratorLayersPtr generate_raft_base( diff --git a/src/libslic3r/Support/SupportMaterial.cpp b/src/libslic3r/Support/SupportMaterial.cpp index 347278911..a21a48b9a 100644 --- a/src/libslic3r/Support/SupportMaterial.cpp +++ b/src/libslic3r/Support/SupportMaterial.cpp @@ -335,14 +335,16 @@ void PrintObjectSupportMaterial::generate(PrintObject &object) // Propagate top / bottom contact layers to generate interface layers // and base interface layers (for soluble interface / non souble base only) - auto [interface_layers, base_interface_layers] = this->generate_interface_layers(bottom_contacts, top_contacts, intermediate_layers, layer_storage); + SupportGeneratorLayersPtr empty_layers; + auto [interface_layers, base_interface_layers] = FFFSupport::generate_interface_layers( + *m_object_config, m_support_params, bottom_contacts, top_contacts, empty_layers, empty_layers, intermediate_layers, layer_storage); BOOST_LOG_TRIVIAL(info) << "Support generator - Creating raft"; // If raft is to be generated, the 1st top_contact layer will contain the 1st object layer silhouette with holes filled. // There is also a 1st intermediate layer containing bases of support columns. // Inflate the bases of the support columns and create the raft base under the object. - SupportGeneratorLayersPtr raft_layers = generate_raft_base(object, m_support_params, m_slicing_params, top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage); + SupportGeneratorLayersPtr raft_layers = FFFSupport::generate_raft_base(object, m_support_params, m_slicing_params, top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage); #ifdef SLIC3R_DEBUG for (const SupportGeneratorLayer *l : interface_layers) @@ -2514,169 +2516,6 @@ void PrintObjectSupportMaterial::trim_support_layers_by_object( BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::trim_support_layers_by_object() in parallel - end"; } -// Convert some of the intermediate layers into top/bottom interface layers as well as base interface layers. -std::pair PrintObjectSupportMaterial::generate_interface_layers( - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayersPtr &intermediate_layers, - SupportGeneratorLayerStorage &layer_storage) const -{ -// my $area_threshold = $self->interface_flow->scaled_spacing ** 2; - - std::pair base_and_interface_layers; - SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first; - SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second; - - // distinguish between interface and base interface layers - // Contact layer is considered an interface layer, therefore run the following block only if support_material_interface_layers > 1. - // Contact layer needs a base_interface layer, therefore run the following block if support_material_interface_layers > 0, has soluble support and extruders are different. - bool soluble_interface_non_soluble_base = - // Zero z-gap between the overhangs and the support interface. - m_slicing_params.soluble_interface && - // Interface extruder soluble. - m_object_config->support_material_interface_extruder.value > 0 && m_print_config->filament_soluble.get_at(m_object_config->support_material_interface_extruder.value - 1) && - // Base extruder: Either "print with active extruder" not soluble. - (m_object_config->support_material_extruder.value == 0 || ! m_print_config->filament_soluble.get_at(m_object_config->support_material_extruder.value - 1)); - bool snug_supports = m_object_config->support_material_style.value != smsGrid; - int num_interface_layers_top = m_object_config->support_material_interface_layers; - int num_interface_layers_bottom = m_object_config->support_material_bottom_interface_layers; - if (num_interface_layers_bottom < 0) - num_interface_layers_bottom = num_interface_layers_top; - int num_base_interface_layers_top = soluble_interface_non_soluble_base ? std::min(num_interface_layers_top / 2, 2) : 0; - int num_base_interface_layers_bottom = soluble_interface_non_soluble_base ? std::min(num_interface_layers_bottom / 2, 2) : 0; - - if (! intermediate_layers.empty() && (num_interface_layers_top > 1 || num_interface_layers_bottom > 1)) { - // For all intermediate layers, collect top contact surfaces, which are not further than support_material_interface_layers. - BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - start"; - // Since the intermediate layer index starts at zero the number of interface layer needs to be reduced by 1. - -- num_interface_layers_top; - -- num_interface_layers_bottom; - int num_interface_layers_only_top = num_interface_layers_top - num_base_interface_layers_top; - int num_interface_layers_only_bottom = num_interface_layers_bottom - num_base_interface_layers_bottom; - interface_layers.assign(intermediate_layers.size(), nullptr); - if (num_base_interface_layers_top || num_base_interface_layers_bottom) - base_interface_layers.assign(intermediate_layers.size(), nullptr); - auto smoothing_distance = m_support_params.support_material_interface_flow.scaled_spacing() * 1.5; - auto minimum_island_radius = m_support_params.support_material_interface_flow.scaled_spacing() / m_support_params.interface_density; - auto closing_distance = smoothing_distance; // scaled(m_object_config->support_material_closing_radius.value); - // Insert a new layer into base_interface_layers, if intersection with base exists. - auto insert_layer = [&layer_storage, snug_supports, closing_distance, smoothing_distance, minimum_island_radius]( - SupportGeneratorLayer &intermediate_layer, Polygons &bottom, Polygons &&top, const Polygons *subtract, SupporLayerType type) -> SupportGeneratorLayer* { - assert(! bottom.empty() || ! top.empty()); - // Merge top into bottom, unite them with a safety offset. - append(bottom, std::move(top)); - // Merge top / bottom interfaces. For snug supports, merge using closing distance and regularize (close concave corners). - bottom = intersection( - snug_supports ? - smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : - union_safety_offset(std::move(bottom)), - intermediate_layer.polygons); - if (! bottom.empty()) { - //FIXME Remove non-printable tiny islands, let them be printed using the base support. - //bottom = opening(std::move(bottom), minimum_island_radius); - if (! bottom.empty()) { - SupportGeneratorLayer &layer_new = layer_storage.allocate(type); - layer_new.polygons = std::move(bottom); - layer_new.print_z = intermediate_layer.print_z; - layer_new.bottom_z = intermediate_layer.bottom_z; - layer_new.height = intermediate_layer.height; - layer_new.bridging = intermediate_layer.bridging; - // Subtract the interface from the base regions. - intermediate_layer.polygons = diff(intermediate_layer.polygons, layer_new.polygons); - if (subtract) - // Trim the base interface layer with the interface layer. - layer_new.polygons = diff(std::move(layer_new.polygons), *subtract); - //FIXME filter layer_new.polygons islands by a minimum area? - // $interface_area = [ grep abs($_->area) >= $area_threshold, @$interface_area ]; - return &layer_new; - } - } - return nullptr; - }; - tbb::parallel_for(tbb::blocked_range(0, int(intermediate_layers.size())), - [&bottom_contacts, &top_contacts, &intermediate_layers, &insert_layer, - num_interface_layers_top, num_interface_layers_bottom, num_base_interface_layers_top, num_base_interface_layers_bottom, num_interface_layers_only_top, num_interface_layers_only_bottom, - snug_supports, &interface_layers, &base_interface_layers](const tbb::blocked_range& range) { - // Gather the top / bottom contact layers intersecting with num_interface_layers resp. num_interface_layers_only intermediate layers above / below - // this intermediate layer. - // Index of the first top contact layer intersecting the current intermediate layer. - auto idx_top_contact_first = -1; - // Index of the first bottom contact layer intersecting the current intermediate layer. - auto idx_bottom_contact_first = -1; - auto num_intermediate = int(intermediate_layers.size()); - for (int idx_intermediate_layer = range.begin(); idx_intermediate_layer < range.end(); ++ idx_intermediate_layer) { - SupportGeneratorLayer &intermediate_layer = *intermediate_layers[idx_intermediate_layer]; - Polygons polygons_top_contact_projected_interface; - Polygons polygons_top_contact_projected_base; - Polygons polygons_bottom_contact_projected_interface; - Polygons polygons_bottom_contact_projected_base; - if (num_interface_layers_top > 0) { - // Top Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces - coordf_t top_z = intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + num_interface_layers_top - 1)]->print_z; - coordf_t top_inteface_z = std::numeric_limits::max(); - if (num_base_interface_layers_top > 0) - // Some top base interface layers will be generated. - top_inteface_z = num_interface_layers_only_top == 0 ? - // Only base interface layers to generate. - - std::numeric_limits::max() : - intermediate_layers[std::min(num_intermediate - 1, idx_intermediate_layer + num_interface_layers_only_top - 1)]->print_z; - // Move idx_top_contact_first up until above the current print_z. - idx_top_contact_first = idx_higher_or_equal(top_contacts, idx_top_contact_first, [&intermediate_layer](const SupportGeneratorLayer *layer){ return layer->print_z >= intermediate_layer.print_z; }); // - EPSILON - // Collect the top contact areas above this intermediate layer, below top_z. - for (int idx_top_contact = idx_top_contact_first; idx_top_contact < int(top_contacts.size()); ++ idx_top_contact) { - const SupportGeneratorLayer &top_contact_layer = *top_contacts[idx_top_contact]; - //FIXME maybe this adds one interface layer in excess? - if (top_contact_layer.bottom_z - EPSILON > top_z) - break; - polygons_append(top_contact_layer.bottom_z - EPSILON > top_inteface_z ? polygons_top_contact_projected_base : polygons_top_contact_projected_interface, - // For snug supports, project the overhang polygons covering the whole overhang, so that they will merge without a gap with support polygons of the other layers. - // For grid supports, merging of support regions will be performed by the projection into grid. - snug_supports ? *top_contact_layer.overhang_polygons : top_contact_layer.polygons); - } - } - if (num_interface_layers_bottom > 0) { - // Bottom Z coordinate of a slab, over which we are collecting the top / bottom contact surfaces - coordf_t bottom_z = intermediate_layers[std::max(0, idx_intermediate_layer - num_interface_layers_bottom + 1)]->bottom_z; - coordf_t bottom_interface_z = - std::numeric_limits::max(); - if (num_base_interface_layers_bottom > 0) - // Some bottom base interface layers will be generated. - bottom_interface_z = num_interface_layers_only_bottom == 0 ? - // Only base interface layers to generate. - std::numeric_limits::max() : - intermediate_layers[std::max(0, idx_intermediate_layer - num_interface_layers_only_bottom)]->bottom_z; - // Move idx_bottom_contact_first up until touching bottom_z. - idx_bottom_contact_first = idx_higher_or_equal(bottom_contacts, idx_bottom_contact_first, [bottom_z](const SupportGeneratorLayer *layer){ return layer->print_z >= bottom_z - EPSILON; }); - // Collect the top contact areas above this intermediate layer, below top_z. - for (int idx_bottom_contact = idx_bottom_contact_first; idx_bottom_contact < int(bottom_contacts.size()); ++ idx_bottom_contact) { - const SupportGeneratorLayer &bottom_contact_layer = *bottom_contacts[idx_bottom_contact]; - if (bottom_contact_layer.print_z - EPSILON > intermediate_layer.bottom_z) - break; - polygons_append(bottom_contact_layer.print_z - EPSILON > bottom_interface_z ? polygons_bottom_contact_projected_interface : polygons_bottom_contact_projected_base, bottom_contact_layer.polygons); - } - } - SupportGeneratorLayer *interface_layer = nullptr; - if (! polygons_bottom_contact_projected_interface.empty() || ! polygons_top_contact_projected_interface.empty()) { - interface_layer = insert_layer( - intermediate_layer, polygons_bottom_contact_projected_interface, std::move(polygons_top_contact_projected_interface), nullptr, - polygons_top_contact_projected_interface.empty() ? SupporLayerType::BottomInterface : SupporLayerType::TopInterface); - interface_layers[idx_intermediate_layer] = interface_layer; - } - if (! polygons_bottom_contact_projected_base.empty() || ! polygons_top_contact_projected_base.empty()) - base_interface_layers[idx_intermediate_layer] = insert_layer( - intermediate_layer, polygons_bottom_contact_projected_base, std::move(polygons_top_contact_projected_base), - interface_layer ? &interface_layer->polygons : nullptr, SupporLayerType::Base); - } - }); - - // Compress contact_out, remove the nullptr items. - remove_nulls(interface_layers); - remove_nulls(base_interface_layers); - BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - end"; - } - - return base_and_interface_layers; -} - /* void PrintObjectSupportMaterial::clip_by_pillars( const PrintObject &object, diff --git a/src/libslic3r/Support/SupportMaterial.hpp b/src/libslic3r/Support/SupportMaterial.hpp index 924ed1a05..4f1768fb1 100644 --- a/src/libslic3r/Support/SupportMaterial.hpp +++ b/src/libslic3r/Support/SupportMaterial.hpp @@ -72,16 +72,6 @@ private: SupportGeneratorLayersPtr &intermediate_layers, const std::vector &layer_support_areas) const; - // Turn some of the base layers into base interface layers. - // For soluble interfaces with non-soluble bases, print maximum two first interface layers with the base - // extruder to improve adhesion of the soluble filament to the base. - std::pair generate_interface_layers( - const SupportGeneratorLayersPtr &bottom_contacts, - const SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayersPtr &intermediate_layers, - SupportGeneratorLayerStorage &layer_storage) const; - - // Trim support layers by an object to leave a defined gap between // the support volume and the object. void trim_support_layers_by_object( diff --git a/src/libslic3r/Support/SupportParameters.cpp b/src/libslic3r/Support/SupportParameters.cpp index 1c7f860b8..531e8dcaf 100644 --- a/src/libslic3r/Support/SupportParameters.cpp +++ b/src/libslic3r/Support/SupportParameters.cpp @@ -11,6 +11,33 @@ SupportParameters::SupportParameters(const PrintObject &object) const PrintObjectConfig &object_config = object.config(); const SlicingParameters &slicing_params = object.slicing_parameters(); + this->soluble_interface = slicing_params.soluble_interface; + this->soluble_interface_non_soluble_base = + // Zero z-gap between the overhangs and the support interface. + slicing_params.soluble_interface && + // Interface extruder soluble. + object_config.support_material_interface_extruder.value > 0 && print_config.filament_soluble.get_at(object_config.support_material_interface_extruder.value - 1) && + // Base extruder: Either "print with active extruder" not soluble. + (object_config.support_material_extruder.value == 0 || ! print_config.filament_soluble.get_at(object_config.support_material_extruder.value - 1)); + + { + int num_top_interface_layers = std::max(0, object_config.support_material_interface_layers.value); + int num_bottom_interface_layers = object_config.support_material_bottom_interface_layers < 0 ? + num_top_interface_layers : object_config.support_material_bottom_interface_layers; + this->has_top_contacts = num_top_interface_layers > 0; + this->has_bottom_contacts = num_bottom_interface_layers > 0; + this->num_top_interface_layers = this->has_top_contacts ? size_t(num_top_interface_layers - 1) : 0; + this->num_bottom_interface_layers = this->has_bottom_contacts ? size_t(num_bottom_interface_layers - 1) : 0; + if (this->soluble_interface_non_soluble_base) { + // Try to support soluble dense interfaces with non-soluble dense interfaces. + this->num_top_base_interface_layers = size_t(std::min(num_top_interface_layers / 2, 2)); + this->num_bottom_base_interface_layers = size_t(std::min(num_bottom_interface_layers / 2, 2)); + } else { + this->num_top_base_interface_layers = 0; + this->num_bottom_base_interface_layers = 0; + } + } + this->first_layer_flow = Slic3r::support_material_1st_layer_flow(&object, float(slicing_params.first_print_layer_height)); this->support_material_flow = Slic3r::support_material_flow(&object, float(slicing_params.layer_height)); this->support_material_interface_flow = Slic3r::support_material_interface_flow(&object, float(slicing_params.layer_height)); @@ -54,7 +81,6 @@ SupportParameters::SupportParameters(const PrintObject &object) this->can_merge_support_regions = true; } - double interface_spacing = object_config.support_material_interface_spacing.value + this->support_material_interface_flow.spacing(); this->interface_density = std::min(1., this->support_material_interface_flow.spacing() / interface_spacing); double raft_interface_spacing = object_config.support_material_interface_spacing.value + this->raft_interface_flow.spacing(); diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp index 904e8ffe2..be38e9650 100644 --- a/src/libslic3r/Support/SupportParameters.hpp +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -14,6 +14,30 @@ namespace FFFSupport { struct SupportParameters { SupportParameters(const PrintObject &object); + // Both top / bottom contacts and interfaces are soluble. + bool soluble_interface; + // Support contact & interface are soluble, but support base is non-soluble. + bool soluble_interface_non_soluble_base; + + // Is there at least a top contact layer extruded above support base? + bool has_top_contacts; + // Is there at least a bottom contact layer extruded below support base? + bool has_bottom_contacts; + // Number of top interface layers without counting the contact layer. + size_t num_top_interface_layers; + // Number of bottom interface layers without counting the contact layer. + size_t num_bottom_interface_layers; + // Number of top base interface layers. Zero if not soluble_interface_non_soluble_base. + size_t num_top_base_interface_layers; + // Number of bottom base interface layers. Zero if not soluble_interface_non_soluble_base. + size_t num_bottom_base_interface_layers; + + bool has_contacts() const { return this->has_top_contacts || this->has_bottom_contacts; } + bool has_interfaces() const { return this->num_top_interface_layers + this->num_bottom_interface_layers > 0; } + bool has_base_interfaces() const { return this->num_top_base_interface_layers + this->num_bottom_base_interface_layers > 0; } + size_t num_top_interface_layers_only() const { return this->num_top_interface_layers - this->num_top_base_interface_layers; } + size_t num_bottom_interface_layers_only() const { return this->num_bottom_interface_layers - this->num_bottom_base_interface_layers; } + // Flow at the 1st print layer. Flow first_layer_flow; // Flow at the support base (neither top, nor bottom interface). diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index d1306962e..dfeb3fdf9 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -87,7 +87,6 @@ TreeSupportSettings::TreeSupportSettings(const TreeSupportMeshGroupSettings& mes bp_radius_increase_per_layer(std::min(tan(0.7) * layer_height, 0.5 * support_line_width)), z_distance_bottom_layers(size_t(round(double(mesh_group_settings.support_bottom_distance) / double(layer_height)))), z_distance_top_layers(size_t(round(double(mesh_group_settings.support_top_distance) / double(layer_height)))), - performance_interface_skip_layers(round_up_divide(mesh_group_settings.support_interface_skip_height, layer_height)), // support_infill_angles(mesh_group_settings.support_infill_angles), support_roof_angles(mesh_group_settings.support_roof_angles), roof_pattern(mesh_group_settings.support_roof_pattern), @@ -1077,71 +1076,53 @@ void finalize_raft_contact( } } +// Used by generate_initial_areas() in parallel by multiple layers. class InterfacePlacer { public: - InterfacePlacer(const SlicingParameters &slicing_parameters, const TreeModelVolumes &volumes, const TreeSupportSettings &config, bool force_tip_to_roof, size_t num_support_layers, - std::vector &move_bounds, SupportGeneratorLayerStorage &layer_storage, SupportGeneratorLayersPtr &top_contacts) : - slicing_parameters(slicing_parameters), volumes(volumes), config(config), force_tip_to_roof(force_tip_to_roof), - move_bounds(move_bounds), layer_storage(layer_storage), top_contacts(top_contacts) { + InterfacePlacer( + const SlicingParameters &slicing_parameters, + const SupportParameters &support_parameters, + const TreeModelVolumes &volumes, const TreeSupportSettings &config, + bool force_tip_to_roof, size_t num_support_layers, + std::vector &move_bounds, + SupportGeneratorLayerStorage &layer_storage, + SupportGeneratorLayersPtr &top_contacts, SupportGeneratorLayersPtr &top_interfaces, SupportGeneratorLayersPtr &top_base_interfaces) + : + slicing_parameters(slicing_parameters), support_parameters(support_parameters), volumes(volumes), config(config), force_tip_to_roof(force_tip_to_roof), + move_bounds(move_bounds), + layer_storage(layer_storage), top_contacts(top_contacts), top_interfaces(top_interfaces), top_base_interfaces(top_base_interfaces) { m_already_inserted.assign(num_support_layers, {}); this->min_xy_dist = config.xy_distance > config.xy_min_distance; } const SlicingParameters &slicing_parameters; + const SupportParameters &support_parameters; const TreeModelVolumes &volumes; const TreeSupportSettings &config; - bool force_tip_to_roof; + // Radius of the tree tip is large enough to be covered by an interface. + const bool force_tip_to_roof; bool min_xy_dist; - // Outputs - std::vector &move_bounds; - SupportGeneratorLayerStorage &layer_storage; - SupportGeneratorLayersPtr &top_contacts; - -private: - // Temps - static constexpr const auto m_base_radius = scaled(0.01); - const Polygon m_base_circle { make_circle(m_base_radius, SUPPORT_TREE_CIRCLE_RESOLUTION) }; - - // Mutexes, guards - std::mutex m_mutex_movebounds; - std::mutex m_mutex_layer_storage; - std::vector> m_already_inserted; - public: - void add_roof_unguarded(Polygons &&new_roofs, const size_t insert_layer_idx) - { - SupportGeneratorLayer*& l = top_contacts[insert_layer_idx]; - if (l == nullptr) - l = &layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, insert_layer_idx); - // will be unioned in finalize_interface_and_support_areas() - append(l->polygons, std::move(new_roofs)); - } - - void add_roof(Polygons &&new_roofs, const size_t insert_layer_idx) - { - std::lock_guard lock(m_mutex_layer_storage); - add_roof_unguarded(std::move(new_roofs), insert_layer_idx); - } - - void add_roofs(std::vector &&new_roofs, const size_t insert_layer_idx, const size_t dtt_roof) + // called by sample_overhang_area() + // Insert the contact layer and some of the inteface and base interface layers below. + void add_roofs(std::vector &&new_roofs, const size_t insert_layer_idx) { if (! new_roofs.empty()) { std::lock_guard lock(m_mutex_layer_storage); - for (size_t idx = 0; idx < dtt_roof; ++ idx) + for (size_t idx = 0; idx < new_roofs.size(); ++ idx) if (! new_roofs[idx].empty()) - add_roof_unguarded(std::move(new_roofs[idx]), insert_layer_idx - idx); + add_roof_unguarded(std::move(new_roofs[idx]), insert_layer_idx - idx, idx); } } - void add_roof_build_plate(Polygons &&overhang_areas) + // called by sample_overhang_area() + void add_roof_build_plate(Polygons &&overhang_areas, size_t dtt_roof) { std::lock_guard lock(m_mutex_layer_storage); - SupportGeneratorLayer*& l = top_contacts[0]; - if (l == nullptr) - l = &layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, slicing_parameters, config, 0); - append(l->polygons, std::move(overhang_areas)); + this->add_roof_unguarded(std::move(overhang_areas), 0, dtt_roof); } + // called by sample_overhang_area() void add_points_along_lines( // Insert points (tree tips or top contact interfaces) along these lines. LineInformations lines, @@ -1150,7 +1131,7 @@ public: // Insert this number of interface layers. size_t roof_tip_layers, // True if an interface is already generated above these lines. - bool supports_roof, + size_t supports_roof_layers, // The element tries to not move until this dtt is reached. size_t dont_move_until) { @@ -1203,7 +1184,7 @@ public: roof_circle.translate(p.first); new_roofs.emplace_back(std::move(roof_circle)); } - this->add_roof(std::move(new_roofs), this_layer_idx); + this->add_roof(std::move(new_roofs), this_layer_idx, dtt_roof_tip + supports_roof_layers); } for (const LineInformation &line : lines) { @@ -1215,11 +1196,33 @@ public: // don't move until dont_move_until > dtt_roof_tip ? dont_move_until - dtt_roof_tip : 0, // supports roof - dtt_roof_tip > 0 || supports_roof, + dtt_roof_tip + supports_roof_layers > 0, disable_ovalistation); } } +private: + void add_roof_unguarded(Polygons &&new_roofs, const size_t insert_layer_idx, const size_t dtt_roof) + { + SupportGeneratorLayersPtr &layers = + dtt_roof == 0 ? this->top_contacts : + dtt_roof <= support_parameters.num_top_interface_layers_only() ? this->top_interfaces : this->top_base_interfaces; + SupportGeneratorLayer*& l = layers[insert_layer_idx]; + if (l == nullptr) + l = &layer_allocate_unguarded(layer_storage, dtt_roof == 0 ? SupporLayerType::TopContact : SupporLayerType::TopInterface, + slicing_parameters, config, insert_layer_idx); + // will be unioned in finalize_interface_and_support_areas() + append(l->polygons, std::move(new_roofs)); + } + + // called by this->add_points_along_lines() + void add_roof(Polygons &&new_roof, const size_t insert_layer_idx, const size_t dtt_tip) + { + std::lock_guard lock(m_mutex_layer_storage); + add_roof_unguarded(std::move(new_roof), insert_layer_idx, dtt_tip); + } + + // called by this->add_points_along_lines() void add_point_as_influence_area(std::pair p, LayerIndex insert_layer, size_t dont_move_until, bool roof, bool skip_ovalisation) { bool to_bp = p.second == LineStatus::TO_BP || p.second == LineStatus::TO_BP_SAFE; @@ -1233,8 +1236,8 @@ public: Polygons circle{ m_base_circle }; circle.front().translate(p.first); { - std::lock_guard critical_section_movebounds(m_mutex_movebounds); Point hash_pos = p.first / ((config.min_radius + 1) / 10); + std::lock_guard critical_section_movebounds(m_mutex_movebounds); if (!m_already_inserted[insert_layer].count(hash_pos)) { // normalize the point a bit to also catch points which are so close that inserting it would achieve nothing m_already_inserted[insert_layer].emplace(hash_pos); @@ -1261,9 +1264,26 @@ public: move_bounds[insert_layer].emplace_back(state, std::move(circle)); } } - }; + } + + // Outputs + std::vector &move_bounds; + SupportGeneratorLayerStorage &layer_storage; + SupportGeneratorLayersPtr &top_contacts; + SupportGeneratorLayersPtr &top_interfaces; + SupportGeneratorLayersPtr &top_base_interfaces; + + // Temps + static constexpr const auto m_base_radius = scaled(0.01); + const Polygon m_base_circle { make_circle(m_base_radius, SUPPORT_TREE_CIRCLE_RESOLUTION) }; + + // Mutexes, guards + std::mutex m_mutex_movebounds; + std::mutex m_mutex_layer_storage; + std::vector> m_already_inserted; }; +// Called by generate_initial_areas(), used in parallel by multiple layers. // Produce // 1) Maximum num_support_roof_layers roof (top interface & contact) layers. // 2) Tree tips supporting either the roof layers or the object itself. @@ -1280,13 +1300,12 @@ void sample_overhang_area( const bool large_horizontal_roof, // Index of the top suport layer generated by this function. const size_t layer_idx, - // Number of roof (contact, interface) layers between the overhang and tree tips. + // Maximum number of roof (contact, interface) layers between the overhang and tree tips to be generated. const size_t num_support_roof_layers, // const coord_t connect_length, // Configuration classes const TreeSupportMeshGroupSettings &mesh_group_settings, - const SupportParameters &support_params, // Configuration & Output InterfacePlacer &interface_placer) { @@ -1297,11 +1316,12 @@ void sample_overhang_area( // as the pattern may be different one layer below. Same with calculating which points are now no longer being generated as result from // a decreasing roof, as there is no guarantee that a line will be above these points. Implementing a separate roof support behavior // for each pattern harms maintainability as it very well could be >100 LOC - auto generate_roof_lines = [&support_params, &mesh_group_settings](const Polygons &area, LayerIndex layer_idx) -> Polylines { - return generate_support_infill_lines(area, support_params, true, layer_idx, mesh_group_settings.support_roof_line_distance); + auto generate_roof_lines = [&interface_placer, &mesh_group_settings](const Polygons &area, LayerIndex layer_idx) -> Polylines { + return generate_support_infill_lines(area, interface_placer.support_parameters, true, layer_idx, mesh_group_settings.support_roof_line_distance); }; LineInformations overhang_lines; + // Track how many top contact / interface layers were already generated. size_t dtt_roof = 0; size_t layer_generation_dtt = 0; @@ -1325,9 +1345,9 @@ void sample_overhang_area( } Polygons overhang_area_next = diff(overhang_area, forbidden_next); if (area(overhang_area_next) < mesh_group_settings.minimum_roof_area) { - // next layer down the roof area would be to small so we have to insert our roof support here. Also convert squaremicrons to squaremilimeter - if (dtt_roof != 0) { - size_t dtt_before = dtt_roof > 0 ? dtt_roof - 1 : 0; + // Next layer down the roof area would be to small so we have to insert our roof support here. + if (dtt_roof > 0) { + size_t dtt_before = dtt_roof - 1; // Produce support head points supporting an interface layer: First produce the interface lines, then sample them. overhang_lines = split_lines( convert_lines_to_internal(interface_placer.volumes, interface_placer.config, @@ -1354,7 +1374,8 @@ void sample_overhang_area( break; } } - interface_placer.add_roofs(std::move(added_roofs), layer_idx, dtt_roof); + added_roofs.erase(added_roofs.begin() + dtt_roof, added_roofs.end()); + interface_placer.add_roofs(std::move(added_roofs), layer_idx); } if (overhang_lines.empty()) { @@ -1364,7 +1385,7 @@ void sample_overhang_area( bool supports_roof = dtt_roof > 0; bool continuous_tips = ! supports_roof && large_horizontal_roof; Polylines polylines = ensure_maximum_distance_polyline( - generate_support_infill_lines(overhang_area, support_params, supports_roof, layer_idx - layer_generation_dtt, + generate_support_infill_lines(overhang_area, interface_placer.support_parameters, supports_roof, layer_idx - layer_generation_dtt, supports_roof ? mesh_group_settings.support_roof_line_distance : mesh_group_settings.support_tree_branch_distance), continuous_tips ? interface_placer.config.min_radius / 2 : connect_length, 1); size_t point_count = 0; @@ -1388,9 +1409,10 @@ void sample_overhang_area( overhang_lines = convert_lines_to_internal(interface_placer.volumes, interface_placer.config, polylines, layer_idx - dtt_roof); } + assert(dtt_roof <= layer_idx); if (int(dtt_roof) >= layer_idx && large_horizontal_roof) - // reached buildplate - interface_placer.add_roof_build_plate(std::move(overhang_area)); + // Reached buildplate when generating contact, interface and base interface layers. + interface_placer.add_roof_build_plate(std::move(overhang_area), dtt_roof); else { // normal trees have to be generated const bool roof_enabled = num_support_roof_layers > 0; @@ -1401,8 +1423,8 @@ void sample_overhang_area( layer_idx - dtt_roof, // Remaining roof tip layers. interface_placer.force_tip_to_roof ? num_support_roof_layers - dtt_roof : 0, - // Supports roof already? - dtt_roof > 0, + // Supports roof already? How many roof layers were already produced above these tips? + dtt_roof, // Don't move until the following distance to top is reached. roof_enabled ? num_support_roof_layers - dtt_roof : 0); } @@ -1424,7 +1446,8 @@ static void generate_initial_areas( const std::vector &overhangs, std::vector &move_bounds, SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayersPtr &top_interface_layers, + SupportGeneratorLayersPtr &top_interfaces, + SupportGeneratorLayersPtr &top_base_interfaces, SupportGeneratorLayerStorage &layer_storage, std::function throw_on_cancel) { @@ -1462,7 +1485,7 @@ static void generate_initial_areas( ; const size_t num_support_roof_layers = mesh_group_settings.support_roof_layers; const bool roof_enabled = num_support_roof_layers > 0; - const bool force_tip_to_roof = sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area && roof_enabled; + const bool force_tip_to_roof = roof_enabled && sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area; // cap for how much layer below the overhang a new support point may be added, as other than with regular support every new inserted point // may cause extra material and time cost. Could also be an user setting or differently calculated. Idea is that if an overhang // does not turn valid in double the amount of layers a slope of support angle would take to travel xy_distance, nothing reasonable will come from it. @@ -1496,9 +1519,9 @@ static void generate_initial_areas( raw_overhangs.push_back({ layer_idx, &overhangs[overhang_idx] }); } - InterfacePlacer interface_placer{ print_object.slicing_parameters(), volumes, config, force_tip_to_roof, num_support_layers, + InterfacePlacer interface_placer{ print_object.slicing_parameters(), support_params, volumes, config, force_tip_to_roof, num_support_layers, // Outputs - move_bounds, layer_storage, top_contacts }; + move_bounds, layer_storage, top_contacts, top_interfaces, top_base_interfaces }; tbb::parallel_for(tbb::blocked_range(0, raw_overhangs.size()), [&volumes, &config, &raw_overhangs, &mesh_group_settings, &support_params, @@ -1613,7 +1636,7 @@ static void generate_initial_areas( //check_self_intersections(overhang_regular, "overhang_regular3"); for (ExPolygon &roof_part : union_ex(overhang_roofs)) { sample_overhang_area(to_polygons(std::move(roof_part)), true, layer_idx, num_support_roof_layers, connect_length, - mesh_group_settings, support_params, interface_placer); + mesh_group_settings, interface_placer); throw_on_cancel(); } } @@ -1623,9 +1646,8 @@ static void generate_initial_areas( remove_small(overhang_regular, mesh_group_settings.minimum_support_area); for (ExPolygon &support_part : union_ex(overhang_regular)) { sample_overhang_area(to_polygons(std::move(support_part)), - // Don't false, layer_idx, num_support_roof_layers, connect_length, - mesh_group_settings, support_params, interface_placer); + mesh_group_settings, interface_placer); throw_on_cancel(); } } @@ -3392,7 +3414,7 @@ static void finalize_interface_and_support_areas( //FIXME subtract the wipe tower append(floor_layer, intersection(layer_outset, overhangs[sample_layer])); if (layers_below < config.support_bottom_layers) - layers_below = std::min(layers_below + config.performance_interface_skip_layers, config.support_bottom_layers); + layers_below = std::min(layers_below + 1, config.support_bottom_layers); else break; } @@ -4139,11 +4161,20 @@ static void organic_smooth_branches_avoid_collisions( #endif // TREE_SUPPORT_ORGANIC_NUDGE_NEW // Organic specific: Smooth branches and produce one cummulative mesh to be sliced. -static std::vector draw_branches( +static void draw_branches( PrintObject &print_object, TreeModelVolumes &volumes, const TreeSupportSettings &config, std::vector &move_bounds, + + // I/O: + SupportGeneratorLayersPtr &bottom_contacts, + SupportGeneratorLayersPtr &top_contacts, + + // Output: + SupportGeneratorLayersPtr &intermediate_layers, + SupportGeneratorLayerStorage &layer_storage, + std::function throw_on_cancel) { // All SupportElements are put into a layer independent storage to improve parallelization. @@ -4211,7 +4242,7 @@ static std::vector draw_branches( struct Slice { Polygons polygons; - Polygons bottom_interfaces; + Polygons bottom_contacts; size_t num_branches{ 0 }; }; @@ -4316,6 +4347,7 @@ static std::vector draw_branches( [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; std::vector slice_z; + std::vector bottom_contacts; for (size_t tree_id = range.begin(); tree_id < range.end(); ++ tree_id) { Tree &tree = trees[tree_id]; for (const Branch &branch : tree.branches) { @@ -4335,63 +4367,71 @@ static std::vector draw_branches( slice_z.emplace_back(float(0.5 * (bottom_z + print_z))); } std::vector slices = slice_mesh(partial_mesh, slice_z, mesh_slicing_params, throw_on_cancel); + bottom_contacts.clear(); //FIXME parallelize? for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++ i) slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true)); //FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist); size_t num_empty = 0; - if (layer_begin > 0 && branch.has_root && ! branch.path.front()->state.to_model_gracious && ! slices.front().empty()) { - // Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something. - struct BottomExtraSlice { - Polygons polygons; - Polygons supported; - Polygons bottom_interfaces; - double area; - double supported_area; - }; - std::vector bottom_extra_slices; - Polygons rest_support; - coord_t bottom_radius = config.getRadius(branch.path.front()->state); - // Don't propagate further than 1.5 * bottom radius. - //LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height; - LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height; - LayerIndex layer_bottommost = std::max(0, layer_begin - layers_propagate_max); - // Only propagate until the rest area is smaller than this threshold. - double support_area_stop = 0.2 * M_PI * sqr(double(bottom_radius)); - // Only propagate until the rest area is smaller than this threshold. - double support_area_min = 0.1 * M_PI * sqr(double(config.min_radius)); - for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { - rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); - double rest_support_area = area(rest_support); - if (rest_support_area < support_area_stop) - // Don't propagate a fraction of the tree contact surface. - break; - // Measure how much the rest_support is actually supported. - /* - Polygons supported = intersection_clipped(rest_support, volumes.getPlaceableAreas(0, layer_idx, []{})); - double supported_area = area(supported); - printf("Supported area: %d, %lf\n", layer_idx, supported_area); - */ - Polygons supported; - double supported_area; - bottom_extra_slices.push_back({ rest_support, std::move(supported), {}, rest_support_area, supported_area }); - } - // Now remove those bottom slices that are not supported at all. - while (! bottom_extra_slices.empty()) { - Polygons bottom_interfaces = intersection_clipped(bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {})); - if (area(bottom_interfaces) < support_area_min) - bottom_extra_slices.pop_back(); - else { - bottom_extra_slices.back().bottom_interfaces = std::move(bottom_interfaces); + if (slices.front().empty()) { + // Some of the initial layers are empty. + num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); + } else { + if (branch.has_root) { + if (branch.path.front()->state.to_model_gracious) { + if (config.settings.support_floor_layers > 0) + //FIXME one may just take the whole tree slice as bottom interface. + bottom_contacts.emplace_back(intersection_clipped(slices.front(), volumes.getPlaceableAreas(0, layer_begin, [] {}))); + } else if (layer_begin > 0) { + // Drop down areas that do rest non - gracefully on the model to ensure the branch actually rests on something. + struct BottomExtraSlice { + Polygons polygons; + double area; + }; + std::vector bottom_extra_slices; + Polygons rest_support; + coord_t bottom_radius = config.getRadius(branch.path.front()->state); + // Don't propagate further than 1.5 * bottom radius. + //LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height; + LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height; + LayerIndex layer_bottommost = std::max(0, layer_begin - layers_propagate_max); + // Only propagate until the rest area is smaller than this threshold. + double support_area_stop = 0.2 * M_PI * sqr(double(bottom_radius)); + // Only propagate until the rest area is smaller than this threshold. + double support_area_min = 0.1 * M_PI * sqr(double(config.min_radius)); + for (LayerIndex layer_idx = layer_begin - 1; layer_idx >= layer_bottommost; -- layer_idx) { + rest_support = diff_clipped(rest_support.empty() ? slices.front() : rest_support, volumes.getCollision(0, layer_idx, false)); + double rest_support_area = area(rest_support); + if (rest_support_area < support_area_stop) + // Don't propagate a fraction of the tree contact surface. + break; + bottom_extra_slices.push_back({ rest_support, rest_support_area }); + } + // Now remove those bottom slices that are not supported at all. + while (! bottom_extra_slices.empty()) { + Polygons this_bottom_contacts = intersection_clipped( + bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {})); + if (area(this_bottom_contacts) < support_area_min) + bottom_extra_slices.pop_back(); + else if (config.settings.support_floor_layers > 0) + bottom_contacts.emplace_back(std::move(this_bottom_contacts)); + } + if (config.settings.support_floor_layers > 0) + for (int i = int(bottom_extra_slices.size()) - 2; i >= 0; -- i) + bottom_contacts.emplace_back( + intersection_clipped(bottom_extra_slices[i].polygons, volumes.getPlaceableAreas(0, layer_begin - i - 1, [] {}))); + layer_begin -= LayerIndex(bottom_extra_slices.size()); + slices.insert(slices.begin(), bottom_extra_slices.size(), {}); + auto it_dst = slices.begin(); + for (auto it_src = bottom_extra_slices.rbegin(); it_src != bottom_extra_slices.rend(); ++ it_src) + *it_dst ++ = std::move(it_src->polygons); } } - layer_begin -= LayerIndex(bottom_extra_slices.size()); - slices.insert(slices.begin(), bottom_extra_slices.size(), {}); - size_t i = 0; - for (auto it = bottom_extra_slices.rbegin(); it != bottom_extra_slices.rend(); ++it, ++i) - slices[i] = std::move(it->polygons); - } else - num_empty = std::find_if(slices.begin(), slices.end(), [](auto &s) { return !s.empty(); }) - slices.begin(); + if (branch.has_tip) { + // Add top slices to top contacts / interfaces / base interfaces. + //slices; + } + } layer_begin += LayerIndex(num_empty); while (! slices.empty() && slices.back().empty()) { @@ -4414,14 +4454,21 @@ static std::vector draw_branches( tree.slices.insert(tree.slices.begin(), tree.first_layer_id - new_begin, {}); tree.slices.insert(tree.slices.end(), new_size - tree.slices.size(), {}); layer_begin -= LayerIndex(num_empty); - for (LayerIndex i = layer_begin; i != layer_end; ++ i) - if (Polygons &src = slices[i - layer_begin]; ! src.empty()) { + for (LayerIndex i = layer_begin; i != layer_end; ++ i) { + int j = i - layer_begin; + if (Polygons &src = slices[j]; ! src.empty()) { Slice &dst = tree.slices[i - new_begin]; - if (++ dst.num_branches > 1) + if (++ dst.num_branches > 1) { append(dst.polygons, std::move(src)); - else + if (j < bottom_contacts.size()) + append(dst.bottom_contacts, std::move(bottom_contacts[j])); + } else { dst.polygons = std::move(std::move(src)); + if (j < bottom_contacts.size()) + dst.bottom_contacts = std::move(bottom_contacts[j]); + } } + } tree.first_layer_id = new_begin; } } @@ -4434,7 +4481,8 @@ static std::vector draw_branches( Tree &tree = trees[tree_id]; for (Slice &slice : tree.slices) if (slice.num_branches > 1) { - slice.polygons = union_(slice.polygons); + slice.polygons = union_(slice.polygons); + slice.bottom_contacts = union_(slice.bottom_contacts); slice.num_branches = 1; } throw_on_cancel(); @@ -4452,25 +4500,55 @@ static std::vector draw_branches( for (LayerIndex i = tree.first_layer_id; i != tree.first_layer_id + LayerIndex(tree.slices.size()); ++ i) if (Slice &src = tree.slices[i - tree.first_layer_id]; ! src.polygons.empty()) { Slice &dst = slices[i]; - if (++ dst.num_branches > 1) - append(dst.polygons, std::move(src.polygons)); - else - dst.polygons = std::move(src.polygons); + if (++ dst.num_branches > 1) { + append(dst.polygons, std::move(src.polygons)); + append(dst.bottom_contacts, std::move(src.bottom_contacts)); + } else { + dst.polygons = std::move(src.polygons); + dst.bottom_contacts = std::move(src.bottom_contacts); + } } } - std::vector support_layer_storage(move_bounds.size()); tbb::parallel_for(tbb::blocked_range(0, std::min(move_bounds.size(), slices.size()), 1), - [&slices, &support_layer_storage, &throw_on_cancel](const tbb::blocked_range &range) { - for (size_t slice_id = range.begin(); slice_id < range.end(); ++ slice_id) { - Slice &slice = slices[slice_id]; - support_layer_storage[slice_id] = slice.num_branches > 1 ? union_(slice.polygons) : std::move(slice.polygons); + [&print_object, &config, &slices, &bottom_contacts, &top_contacts, &intermediate_layers, &layer_storage, &throw_on_cancel](const tbb::blocked_range &range) { + for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) { + Slice &slice = slices[layer_idx]; + assert(intermediate_layers[layer_idx] == nullptr); + Polygons base_layer_polygons = slice.num_branches > 1 ? union_(slice.polygons) : std::move(slice.polygons); + Polygons bottom_contact_polygons = slice.num_branches > 1 ? union_(slice.bottom_contacts) : std::move(slice.bottom_contacts); + + if (! base_layer_polygons.empty()) { + // Most of the time in this function is this union call. Can take 300+ ms when a lot of areas are to be unioned. + base_layer_polygons = smooth_outward(union_(base_layer_polygons), config.support_line_width); //FIXME was .smooth(50); + //smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : + // simplify a bit, to ensure the output does not contain outrageous amounts of vertices. Should not be necessary, just a precaution. + base_layer_polygons = polygons_simplify(base_layer_polygons, std::min(scaled(0.03), double(config.resolution)), polygons_strictly_simple); + } + + // Subtract top contact layer polygons from support base. + SupportGeneratorLayer *top_contact_layer = top_contacts[layer_idx]; + if (top_contact_layer && ! top_contact_layer->polygons.empty() && ! base_layer_polygons.empty()) { + base_layer_polygons = diff(base_layer_polygons, top_contact_layer->polygons); + if (! bottom_contact_polygons.empty()) + //FIXME it may be better to clip bottom contacts with top contacts first after they are propagated to produce interface layers. + bottom_contact_polygons = diff(bottom_contact_polygons, top_contact_layer->polygons); + } + if (! bottom_contact_polygons.empty()) { + base_layer_polygons = diff(base_layer_polygons, bottom_contact_polygons); + SupportGeneratorLayer *bottom_contact_layer = bottom_contacts[layer_idx] = &layer_allocate( + layer_storage, SupporLayerType::BottomContact, print_object.slicing_parameters(), config, layer_idx); + bottom_contact_layer->polygons = std::move(bottom_contact_polygons); + } + if (! base_layer_polygons.empty()) { + SupportGeneratorLayer *base_layer = intermediate_layers[layer_idx] = &layer_allocate( + layer_storage, SupporLayerType::Base, print_object.slicing_parameters(), config, layer_idx); + base_layer->polygons = union_(base_layer_polygons); + } + throw_on_cancel(); } }, tbb::simple_partitioner()); - - //FIXME simplify! - return support_layer_storage; } /*! @@ -4538,9 +4616,12 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume SupportGeneratorLayerStorage layer_storage; SupportGeneratorLayersPtr top_contacts; SupportGeneratorLayersPtr bottom_contacts; - SupportGeneratorLayersPtr top_interface_layers; + SupportGeneratorLayersPtr interface_layers; + SupportGeneratorLayersPtr base_interface_layers; SupportGeneratorLayersPtr intermediate_layers; + SupportParameters support_params(print_object); + if (size_t num_support_layers = precalculate(print, overhangs, processing.first, processing.second, volumes, throw_on_cancel); num_support_layers > 0) { @@ -4550,13 +4631,15 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume std::vector move_bounds(num_support_layers); // ### Place tips of the support tree - top_contacts .assign(num_support_layers, nullptr); - bottom_contacts .assign(num_support_layers, nullptr); - top_interface_layers.assign(num_support_layers, nullptr); - intermediate_layers .assign(num_support_layers, nullptr); + bottom_contacts .assign(num_support_layers, nullptr); + top_contacts .assign(num_support_layers, nullptr); + interface_layers .assign(num_support_layers, nullptr); + base_interface_layers.assign(num_support_layers, nullptr); + intermediate_layers .assign(num_support_layers, nullptr); for (size_t mesh_idx : processing.second) - generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs, move_bounds, top_contacts, top_interface_layers, layer_storage, throw_on_cancel); + generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs, + move_bounds, top_contacts, interface_layers, base_interface_layers, layer_storage, throw_on_cancel); auto t_gen = std::chrono::high_resolution_clock::now(); #ifdef TREESUPPORT_DEBUG_SVG @@ -4587,12 +4670,24 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel); else { assert(print_object.config().support_material_style == smsOrganic); - std::vector support_layer_storage = draw_branches(*print.get_object(processing.second.front()), volumes, config, move_bounds, throw_on_cancel); - std::vector support_roof_storage(support_layer_storage.size()); - finalize_interface_and_support_areas(print_object, volumes, config, overhangs, support_layer_storage, support_roof_storage, - bottom_contacts, top_contacts, intermediate_layers, layer_storage, throw_on_cancel); + draw_branches( + *print.get_object(processing.second.front()), volumes, config, move_bounds, + bottom_contacts, top_contacts, intermediate_layers, layer_storage, + throw_on_cancel); } + auto remove_undefined_layers = [](SupportGeneratorLayersPtr& layers) { + layers.erase(std::remove_if(layers.begin(), layers.end(), [](const SupportGeneratorLayer* ptr) { return ptr == nullptr; }), layers.end()); + }; + remove_undefined_layers(bottom_contacts); + remove_undefined_layers(top_contacts); + remove_undefined_layers(interface_layers); + remove_undefined_layers(base_interface_layers); + remove_undefined_layers(intermediate_layers); + + std::tie(interface_layers, base_interface_layers) = generate_interface_layers(print_object.config(), support_params, + bottom_contacts, top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage); + auto t_draw = std::chrono::high_resolution_clock::now(); auto dur_pre_gen = 0.001 * std::chrono::duration_cast(t_precalc - t_start).count(); auto dur_gen = 0.001 * std::chrono::duration_cast(t_gen - t_precalc).count(); @@ -4618,18 +4713,10 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume continue; } - auto remove_undefined_layers = [](SupportGeneratorLayersPtr &layers) { - layers.erase(std::remove_if(layers.begin(), layers.end(), [](const SupportGeneratorLayer* ptr) { return ptr == nullptr; }), layers.end()); - }; - remove_undefined_layers(bottom_contacts); - remove_undefined_layers(top_contacts); - remove_undefined_layers(intermediate_layers); - // Produce the support G-code. // Used by both classic and tree supports. - SupportParameters support_params(print_object); - SupportGeneratorLayersPtr interface_layers, base_interface_layers; - SupportGeneratorLayersPtr raft_layers = generate_raft_base(print_object, support_params, print_object.slicing_parameters(), top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage); + SupportGeneratorLayersPtr raft_layers = generate_raft_base(print_object, support_params, print_object.slicing_parameters(), + top_contacts, interface_layers, base_interface_layers, intermediate_layers, layer_storage); #if 1 //#ifdef SLIC3R_DEBUG SupportGeneratorLayersPtr layers_sorted = #endif // SLIC3R_DEBUG diff --git a/src/libslic3r/Support/TreeSupport.hpp b/src/libslic3r/Support/TreeSupport.hpp index 2ed5d50ec..899f02724 100644 --- a/src/libslic3r/Support/TreeSupport.hpp +++ b/src/libslic3r/Support/TreeSupport.hpp @@ -364,10 +364,6 @@ public: * \brief Amount of layers distance required from the top of the model to the bottom of a support structure. */ size_t z_distance_bottom_layers; - /*! - * \brief used for performance optimization at the support floor. Should have no impact on the resulting tree. - */ - size_t performance_interface_skip_layers; /*! * \brief User specified angles for the support infill. */ From c838fc92fc1ebb19791e003dd4481f21d5ee12c8 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Mon, 8 May 2023 10:25:28 +0200 Subject: [PATCH 107/115] Follow-up to 1e7a3216ca87d9fdeaa5b12f813e5900a77d49bc WIP Organic supports intefaces: bugfixes --- src/libslic3r/Support/SupportCommon.cpp | 37 +++++++++++++------------ src/libslic3r/Support/TreeSupport.cpp | 13 +++++++-- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index 4978b9e40..951ae740d 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -137,7 +137,8 @@ std::pair generate_interfa if (! intermediate_layers.empty() && support_params.has_interfaces()) { // For all intermediate layers, collect top contact surfaces, which are not further than support_material_interface_layers. BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - start"; - const bool snug_supports = config.support_material_style.value != smsGrid; + const bool snug_supports = config.support_material_style.value == smsSnug; + const bool smooth_supports = config.support_material_style.value != smsGrid; SupportGeneratorLayersPtr &interface_layers = base_and_interface_layers.first; SupportGeneratorLayersPtr &base_interface_layers = base_and_interface_layers.second; interface_layers.assign(intermediate_layers.size(), nullptr); @@ -147,7 +148,7 @@ std::pair generate_interfa const auto minimum_island_radius = support_params.support_material_interface_flow.scaled_spacing() / support_params.interface_density; const auto closing_distance = smoothing_distance; // scaled(config.support_material_closing_radius.value); // Insert a new layer into base_interface_layers, if intersection with base exists. - auto insert_layer = [&layer_storage, snug_supports, closing_distance, smoothing_distance, minimum_island_radius]( + auto insert_layer = [&layer_storage, smooth_supports, closing_distance, smoothing_distance, minimum_island_radius]( SupportGeneratorLayer &intermediate_layer, Polygons &bottom, Polygons &&top, SupportGeneratorLayer *top_interface_layer, const Polygons *subtract, SupporLayerType type) -> SupportGeneratorLayer* { bool has_top_interface = top_interface_layer && ! top_interface_layer->polygons.empty(); @@ -156,14 +157,16 @@ std::pair generate_interfa append(bottom, std::move(top)); // Merge top / bottom interfaces. For snug supports, merge using closing distance and regularize (close concave corners). bottom = intersection( - snug_supports ? + smooth_supports ? smooth_outward(closing(std::move(bottom), closing_distance + minimum_island_radius, closing_distance, SUPPORT_SURFACES_OFFSET_PARAMETERS), smoothing_distance) : union_safety_offset(std::move(bottom)), intermediate_layer.polygons); - if (has_top_interface) + if (has_top_interface) { // Don't trim the precomputed Organic supports top interface with base layer // as the precomputed top interface likely expands over multiple tree tips. bottom = union_(std::move(top_interface_layer->polygons), bottom); + top_interface_layer->polygons.clear(); + } if (! bottom.empty()) { //FIXME Remove non-printable tiny islands, let them be printed using the base support. //bottom = opening(std::move(bottom), minimum_island_radius); @@ -253,13 +256,8 @@ std::pair generate_interfa auto resolve_same_layer = [](SupportGeneratorLayersPtr &layers, int &idx, coordf_t print_z) -> SupportGeneratorLayer* { if (! layers.empty()) { idx = idx_higher_or_equal(layers, idx, [print_z](const SupportGeneratorLayer *layer) { return layer->print_z > print_z - EPSILON; }); - if (idx < int(layers.size()) && layers[idx]->print_z < print_z + EPSILON) { - SupportGeneratorLayer *l = layers[idx]; - // Remove the layer from the source container, as it will be consumed here: It will be merged - // with the newly produced interfaces. - layers[idx] = nullptr; - return l; - } + if (idx < int(layers.size()) && layers[idx]->print_z < print_z + EPSILON) + return layers[idx]; } return nullptr; }; @@ -284,11 +282,14 @@ std::pair generate_interfa // Compress contact_out, remove the nullptr items. // The parallel_for above may not have merged all the interface and base_interface layers // generated by the Organic supports code, do it here. - auto merge_remove_nulls = [](SupportGeneratorLayersPtr &in1, SupportGeneratorLayersPtr &in2) { - size_t nonzeros = std::count_if(in1.begin(), in1.end(), [](auto *l) { return l != nullptr; }) + - std::count_if(in2.begin(), in2.end(), [](auto *l) { return l != nullptr; }); - remove_nulls(in1); - remove_nulls(in2); + auto merge_remove_empty = [](SupportGeneratorLayersPtr &in1, SupportGeneratorLayersPtr &in2) { + auto remove_empty = [](SupportGeneratorLayersPtr &vec) { + vec.erase( + std::remove_if(vec.begin(), vec.end(), [](const SupportGeneratorLayer *ptr) { return ptr == nullptr || ptr->polygons.empty(); }), + vec.end()); + }; + remove_empty(in1); + remove_empty(in2); if (in2.empty()) return std::move(in1); else if (in1.empty()) @@ -299,8 +300,8 @@ std::pair generate_interfa return std::move(out); } }; - interface_layers = merge_remove_nulls(interface_layers, top_interface_layers); - base_interface_layers = merge_remove_nulls(base_interface_layers, top_base_interface_layers); + interface_layers = merge_remove_empty(interface_layers, top_interface_layers); + base_interface_layers = merge_remove_empty(base_interface_layers, top_base_interface_layers); BOOST_LOG_TRIVIAL(debug) << "PrintObjectSupportMaterial::generate_interface_layers() in parallel - end"; } diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index dfeb3fdf9..e27661384 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -1119,7 +1119,7 @@ public: void add_roof_build_plate(Polygons &&overhang_areas, size_t dtt_roof) { std::lock_guard lock(m_mutex_layer_storage); - this->add_roof_unguarded(std::move(overhang_areas), 0, dtt_roof); + this->add_roof_unguarded(std::move(overhang_areas), 0, std::min(dtt_roof, this->support_parameters.num_top_interface_layers)); } // called by sample_overhang_area() @@ -1204,6 +1204,8 @@ public: private: void add_roof_unguarded(Polygons &&new_roofs, const size_t insert_layer_idx, const size_t dtt_roof) { + assert(support_parameters.has_top_contacts); + assert(dtt_roof <= support_parameters.num_top_interface_layers); SupportGeneratorLayersPtr &layers = dtt_roof == 0 ? this->top_contacts : dtt_roof <= support_parameters.num_top_interface_layers_only() ? this->top_interfaces : this->top_base_interfaces; @@ -4413,8 +4415,13 @@ static void draw_branches( bottom_extra_slices.back().polygons, volumes.getPlaceableAreas(0, layer_begin - LayerIndex(bottom_extra_slices.size()), [] {})); if (area(this_bottom_contacts) < support_area_min) bottom_extra_slices.pop_back(); - else if (config.settings.support_floor_layers > 0) - bottom_contacts.emplace_back(std::move(this_bottom_contacts)); + else { + // At least a fraction of the tree bottom is considered to be supported. + if (config.settings.support_floor_layers > 0) + // Turn this fraction of the tree bottom into a contact layer. + bottom_contacts.emplace_back(std::move(this_bottom_contacts)); + break; + } } if (config.settings.support_floor_layers > 0) for (int i = int(bottom_extra_slices.size()) - 2; i >= 0; -- i) From 9d495f2413c215c80de9eead736e78c1c9142453 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 9 May 2023 09:46:27 +0200 Subject: [PATCH 108/115] Organic supports: Refactoring for adding interfaces to tree tips. --- src/libslic3r/Support/TreeSupport.cpp | 549 ++++++++++++++------------ 1 file changed, 290 insertions(+), 259 deletions(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index e27661384..0eb009c77 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -997,11 +997,245 @@ inline SupportGeneratorLayer& layer_allocate( return layer_initialize(layer, slicing_params, config, layer_idx); } +using SupportElements = std::deque; + +// Used by generate_initial_areas() in parallel by multiple layers. +class InterfacePlacer { +public: + InterfacePlacer( + const SlicingParameters &slicing_parameters, + const SupportParameters &support_parameters, + const TreeSupportSettings &config, + SupportGeneratorLayerStorage &layer_storage, + SupportGeneratorLayersPtr &top_contacts, + SupportGeneratorLayersPtr &top_interfaces, + SupportGeneratorLayersPtr &top_base_interfaces) + : + slicing_parameters(slicing_parameters), support_parameters(support_parameters), config(config), + layer_storage(layer_storage), top_contacts(top_contacts), top_interfaces(top_interfaces), top_base_interfaces(top_base_interfaces) + {} + InterfacePlacer(const InterfacePlacer& rhs) : + slicing_parameters(rhs.slicing_parameters), support_parameters(rhs.support_parameters), config(rhs.config), + layer_storage(rhs.layer_storage), top_contacts(rhs.top_contacts), top_interfaces(rhs.top_interfaces), top_base_interfaces(rhs.top_base_interfaces) + {} + + const SlicingParameters &slicing_parameters; + const SupportParameters &support_parameters; + const TreeSupportSettings &config; + SupportGeneratorLayersPtr& top_contacts_mutable() { return this->top_contacts; } + +public: + // Insert the contact layer and some of the inteface and base interface layers below. + void add_roofs(std::vector &&new_roofs, const size_t insert_layer_idx) + { + if (! new_roofs.empty()) { + std::lock_guard lock(m_mutex_layer_storage); + for (size_t idx = 0; idx < new_roofs.size(); ++ idx) + if (! new_roofs[idx].empty()) + add_roof_unguarded(std::move(new_roofs[idx]), insert_layer_idx - idx, idx); + } + } + + void add_roof(Polygons &&new_roof, const size_t insert_layer_idx, const size_t dtt_tip) + { + std::lock_guard lock(m_mutex_layer_storage); + add_roof_unguarded(std::move(new_roof), insert_layer_idx, dtt_tip); + } + + // called by sample_overhang_area() + void add_roof_build_plate(Polygons &&overhang_areas, size_t dtt_roof) + { + std::lock_guard lock(m_mutex_layer_storage); + this->add_roof_unguarded(std::move(overhang_areas), 0, std::min(dtt_roof, this->support_parameters.num_top_interface_layers)); + } + + void add_roof_unguarded(Polygons &&new_roofs, const size_t insert_layer_idx, const size_t dtt_roof) + { + assert(support_parameters.has_top_contacts); + assert(dtt_roof <= support_parameters.num_top_interface_layers); + SupportGeneratorLayersPtr &layers = + dtt_roof == 0 ? this->top_contacts : + dtt_roof <= support_parameters.num_top_interface_layers_only() ? this->top_interfaces : this->top_base_interfaces; + SupportGeneratorLayer*& l = layers[insert_layer_idx]; + if (l == nullptr) + l = &layer_allocate_unguarded(layer_storage, dtt_roof == 0 ? SupporLayerType::TopContact : SupporLayerType::TopInterface, + slicing_parameters, config, insert_layer_idx); + // will be unioned in finalize_interface_and_support_areas() + append(l->polygons, std::move(new_roofs)); + } + +private: + // Outputs + SupportGeneratorLayerStorage &layer_storage; + SupportGeneratorLayersPtr &top_contacts; + SupportGeneratorLayersPtr &top_interfaces; + SupportGeneratorLayersPtr &top_base_interfaces; + + // Mutexes, guards + std::mutex m_mutex_layer_storage; +}; + +class RichInterfacePlacer : public InterfacePlacer { +public: + RichInterfacePlacer( + const InterfacePlacer &interface_placer, + const TreeModelVolumes &volumes, + bool force_tip_to_roof, + size_t num_support_layers, + std::vector &move_bounds) + : + InterfacePlacer(interface_placer), + volumes(volumes), force_tip_to_roof(force_tip_to_roof), move_bounds(move_bounds) + { + m_already_inserted.assign(num_support_layers, {}); + this->min_xy_dist = this->config.xy_distance > this->config.xy_min_distance; + } + const TreeModelVolumes &volumes; + // Radius of the tree tip is large enough to be covered by an interface. + const bool force_tip_to_roof; + bool min_xy_dist; + +public: + // called by sample_overhang_area() + void add_points_along_lines( + // Insert points (tree tips or top contact interfaces) along these lines. + LineInformations lines, + // Start at this layer. + LayerIndex insert_layer_idx, + // Insert this number of interface layers. + size_t roof_tip_layers, + // True if an interface is already generated above these lines. + size_t supports_roof_layers, + // The element tries to not move until this dtt is reached. + size_t dont_move_until) + { + validate_range(lines); + // Add tip area as roof (happens when minimum roof area > minimum tip area) if possible + size_t dtt_roof_tip; + for (dtt_roof_tip = 0; dtt_roof_tip < roof_tip_layers && insert_layer_idx - dtt_roof_tip >= 1; ++ dtt_roof_tip) { + size_t this_layer_idx = insert_layer_idx - dtt_roof_tip; + auto evaluateRoofWillGenerate = [&](const std::pair &p) { + //FIXME Vojtech: The circle is just shifted, it has a known size, the infill should fit all the time! + #if 0 + Polygon roof_circle; + for (Point corner : base_circle) + roof_circle.points.emplace_back(p.first + corner * config.min_radius); + return !generate_support_infill_lines({ roof_circle }, config, true, insert_layer_idx - dtt_roof_tip, config.support_roof_line_distance).empty(); + #else + return true; + #endif + }; + + { + std::pair split = + // keep all lines that are still valid on the next layer + split_lines(lines, [this, this_layer_idx](const std::pair &p) + { return evaluate_point_for_next_layer_function(volumes, config, this_layer_idx, p); }); + LineInformations points = std::move(split.second); + // Not all roofs are guaranteed to actually generate lines, so filter these out and add them as points. + split = split_lines(split.first, evaluateRoofWillGenerate); + lines = std::move(split.first); + append(points, split.second); + // add all points that would not be valid + for (const LineInformation &line : points) + for (const std::pair &point_data : line) + add_point_as_influence_area(point_data, this_layer_idx, + // don't move until + roof_tip_layers - dtt_roof_tip, + // supports roof + dtt_roof_tip + supports_roof_layers > 0, + // disable ovalization + false); + } + + // add all tips as roof to the roof storage + Polygons new_roofs; + for (const LineInformation &line : lines) + //FIXME sweep the tip radius along the line? + for (const std::pair &p : line) { + Polygon roof_circle{ m_base_circle }; + roof_circle.scale(config.min_radius / m_base_radius); + roof_circle.translate(p.first); + new_roofs.emplace_back(std::move(roof_circle)); + } + this->add_roof(std::move(new_roofs), this_layer_idx, dtt_roof_tip + supports_roof_layers); + } + + for (const LineInformation &line : lines) { + // If a line consists of enough tips, the assumption is that it is not a single tip, but part of a simulated support pattern. + // Ovalisation should be disabled for these to improve the quality of the lines when tip_diameter=line_width + bool disable_ovalistation = config.min_radius < 3 * config.support_line_width && roof_tip_layers == 0 && dtt_roof_tip == 0 && line.size() > 5; + for (const std::pair &point_data : line) + add_point_as_influence_area(point_data, insert_layer_idx - dtt_roof_tip, + // don't move until + dont_move_until > dtt_roof_tip ? dont_move_until - dtt_roof_tip : 0, + // supports roof + dtt_roof_tip + supports_roof_layers > 0, + disable_ovalistation); + } + } + +private: + // called by this->add_points_along_lines() + void add_point_as_influence_area(std::pair p, LayerIndex insert_layer, size_t dont_move_until, bool roof, bool skip_ovalisation) + { + bool to_bp = p.second == LineStatus::TO_BP || p.second == LineStatus::TO_BP_SAFE; + bool gracious = to_bp || p.second == LineStatus::TO_MODEL_GRACIOUS || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; + bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; + if (! config.support_rests_on_model && ! to_bp) { + BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point"; + tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true); + return; + } + Polygons circle{ m_base_circle }; + circle.front().translate(p.first); + { + Point hash_pos = p.first / ((config.min_radius + 1) / 10); + std::lock_guard critical_section_movebounds(m_mutex_movebounds); + if (!m_already_inserted[insert_layer].count(hash_pos)) { + // normalize the point a bit to also catch points which are so close that inserting it would achieve nothing + m_already_inserted[insert_layer].emplace(hash_pos); + static constexpr const size_t dtt = 0; + SupportElementState state; + state.target_height = insert_layer; + state.target_position = p.first; + state.next_position = p.first; + state.layer_idx = insert_layer; + state.effective_radius_height = dtt; + state.to_buildplate = to_bp; + state.distance_to_top = dtt; + state.result_on_layer = p.first; + assert(state.result_on_layer_is_set()); + state.increased_to_model_radius = 0; + state.to_model_gracious = gracious; + state.elephant_foot_increases = 0; + state.use_min_xy_dist = min_xy_dist; + state.supports_roof = roof; + state.dont_move_until = dont_move_until; + state.can_use_safe_radius = safe_radius; + state.missing_roof_layers = force_tip_to_roof ? dont_move_until : 0; + state.skip_ovalisation = skip_ovalisation; + move_bounds[insert_layer].emplace_back(state, std::move(circle)); + } + } + } + + // Outputs + std::vector &move_bounds; + + // Temps + static constexpr const auto m_base_radius = scaled(0.01); + const Polygon m_base_circle { make_circle(m_base_radius, SUPPORT_TREE_CIRCLE_RESOLUTION) }; + + // Mutexes, guards + std::mutex m_mutex_movebounds; + std::vector> m_already_inserted; +}; + int generate_raft_contact( const PrintObject &print_object, const TreeSupportSettings &config, - SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayerStorage &layer_storage) + InterfacePlacer &interface_placer) { int raft_contact_layer_idx = -1; if (print_object.has_raft() && print_object.layer_count() > 0) { @@ -1012,17 +1246,13 @@ int generate_raft_contact( while (raft_contact_layer_idx > 0 && config.raft_layers[raft_contact_layer_idx] > print_object.slicing_parameters().raft_contact_top_z + EPSILON) -- raft_contact_layer_idx; // Create the raft contact layer. - SupportGeneratorLayer &raft_contact_layer = layer_allocate_unguarded(layer_storage, SupporLayerType::TopContact, print_object.slicing_parameters(), config, raft_contact_layer_idx); - top_contacts[raft_contact_layer_idx] = &raft_contact_layer; const ExPolygons &lslices = print_object.get_layer(0)->lslices; double expansion = print_object.config().raft_expansion.value; - raft_contact_layer.polygons = expansion > 0 ? expand(lslices, scaled(expansion)) : to_polygons(lslices); + interface_placer.add_roof_unguarded(expansion > 0 ? expand(lslices, scaled(expansion)) : to_polygons(lslices), raft_contact_layer_idx, 0); } return raft_contact_layer_idx; } -using SupportElements = std::deque; - void finalize_raft_contact( const PrintObject &print_object, const int raft_contact_layer_idx, @@ -1076,215 +1306,6 @@ void finalize_raft_contact( } } -// Used by generate_initial_areas() in parallel by multiple layers. -class InterfacePlacer { -public: - InterfacePlacer( - const SlicingParameters &slicing_parameters, - const SupportParameters &support_parameters, - const TreeModelVolumes &volumes, const TreeSupportSettings &config, - bool force_tip_to_roof, size_t num_support_layers, - std::vector &move_bounds, - SupportGeneratorLayerStorage &layer_storage, - SupportGeneratorLayersPtr &top_contacts, SupportGeneratorLayersPtr &top_interfaces, SupportGeneratorLayersPtr &top_base_interfaces) - : - slicing_parameters(slicing_parameters), support_parameters(support_parameters), volumes(volumes), config(config), force_tip_to_roof(force_tip_to_roof), - move_bounds(move_bounds), - layer_storage(layer_storage), top_contacts(top_contacts), top_interfaces(top_interfaces), top_base_interfaces(top_base_interfaces) { - m_already_inserted.assign(num_support_layers, {}); - this->min_xy_dist = config.xy_distance > config.xy_min_distance; - } - const SlicingParameters &slicing_parameters; - const SupportParameters &support_parameters; - const TreeModelVolumes &volumes; - const TreeSupportSettings &config; - // Radius of the tree tip is large enough to be covered by an interface. - const bool force_tip_to_roof; - bool min_xy_dist; - -public: - // called by sample_overhang_area() - // Insert the contact layer and some of the inteface and base interface layers below. - void add_roofs(std::vector &&new_roofs, const size_t insert_layer_idx) - { - if (! new_roofs.empty()) { - std::lock_guard lock(m_mutex_layer_storage); - for (size_t idx = 0; idx < new_roofs.size(); ++ idx) - if (! new_roofs[idx].empty()) - add_roof_unguarded(std::move(new_roofs[idx]), insert_layer_idx - idx, idx); - } - } - - // called by sample_overhang_area() - void add_roof_build_plate(Polygons &&overhang_areas, size_t dtt_roof) - { - std::lock_guard lock(m_mutex_layer_storage); - this->add_roof_unguarded(std::move(overhang_areas), 0, std::min(dtt_roof, this->support_parameters.num_top_interface_layers)); - } - - // called by sample_overhang_area() - void add_points_along_lines( - // Insert points (tree tips or top contact interfaces) along these lines. - LineInformations lines, - // Start at this layer. - LayerIndex insert_layer_idx, - // Insert this number of interface layers. - size_t roof_tip_layers, - // True if an interface is already generated above these lines. - size_t supports_roof_layers, - // The element tries to not move until this dtt is reached. - size_t dont_move_until) - { - validate_range(lines); - // Add tip area as roof (happens when minimum roof area > minimum tip area) if possible - size_t dtt_roof_tip; - for (dtt_roof_tip = 0; dtt_roof_tip < roof_tip_layers && insert_layer_idx - dtt_roof_tip >= 1; ++ dtt_roof_tip) { - size_t this_layer_idx = insert_layer_idx - dtt_roof_tip; - auto evaluateRoofWillGenerate = [&](const std::pair &p) { - //FIXME Vojtech: The circle is just shifted, it has a known size, the infill should fit all the time! - #if 0 - Polygon roof_circle; - for (Point corner : base_circle) - roof_circle.points.emplace_back(p.first + corner * config.min_radius); - return !generate_support_infill_lines({ roof_circle }, config, true, insert_layer_idx - dtt_roof_tip, config.support_roof_line_distance).empty(); - #else - return true; - #endif - }; - - { - std::pair split = - // keep all lines that are still valid on the next layer - split_lines(lines, [this, this_layer_idx](const std::pair &p) - { return evaluate_point_for_next_layer_function(volumes, config, this_layer_idx, p); }); - LineInformations points = std::move(split.second); - // Not all roofs are guaranteed to actually generate lines, so filter these out and add them as points. - split = split_lines(split.first, evaluateRoofWillGenerate); - lines = std::move(split.first); - append(points, split.second); - // add all points that would not be valid - for (const LineInformation &line : points) - for (const std::pair &point_data : line) - add_point_as_influence_area(point_data, this_layer_idx, - // don't move until - roof_tip_layers - dtt_roof_tip, - // supports roof - dtt_roof_tip > 0, - // disable ovalization - false); - } - - // add all tips as roof to the roof storage - Polygons new_roofs; - for (const LineInformation &line : lines) - //FIXME sweep the tip radius along the line? - for (const std::pair &p : line) { - Polygon roof_circle{ m_base_circle }; - roof_circle.scale(config.min_radius / m_base_radius); - roof_circle.translate(p.first); - new_roofs.emplace_back(std::move(roof_circle)); - } - this->add_roof(std::move(new_roofs), this_layer_idx, dtt_roof_tip + supports_roof_layers); - } - - for (const LineInformation &line : lines) { - // If a line consists of enough tips, the assumption is that it is not a single tip, but part of a simulated support pattern. - // Ovalisation should be disabled for these to improve the quality of the lines when tip_diameter=line_width - bool disable_ovalistation = config.min_radius < 3 * config.support_line_width && roof_tip_layers == 0 && dtt_roof_tip == 0 && line.size() > 5; - for (const std::pair &point_data : line) - add_point_as_influence_area(point_data, insert_layer_idx - dtt_roof_tip, - // don't move until - dont_move_until > dtt_roof_tip ? dont_move_until - dtt_roof_tip : 0, - // supports roof - dtt_roof_tip + supports_roof_layers > 0, - disable_ovalistation); - } - } - -private: - void add_roof_unguarded(Polygons &&new_roofs, const size_t insert_layer_idx, const size_t dtt_roof) - { - assert(support_parameters.has_top_contacts); - assert(dtt_roof <= support_parameters.num_top_interface_layers); - SupportGeneratorLayersPtr &layers = - dtt_roof == 0 ? this->top_contacts : - dtt_roof <= support_parameters.num_top_interface_layers_only() ? this->top_interfaces : this->top_base_interfaces; - SupportGeneratorLayer*& l = layers[insert_layer_idx]; - if (l == nullptr) - l = &layer_allocate_unguarded(layer_storage, dtt_roof == 0 ? SupporLayerType::TopContact : SupporLayerType::TopInterface, - slicing_parameters, config, insert_layer_idx); - // will be unioned in finalize_interface_and_support_areas() - append(l->polygons, std::move(new_roofs)); - } - - // called by this->add_points_along_lines() - void add_roof(Polygons &&new_roof, const size_t insert_layer_idx, const size_t dtt_tip) - { - std::lock_guard lock(m_mutex_layer_storage); - add_roof_unguarded(std::move(new_roof), insert_layer_idx, dtt_tip); - } - - // called by this->add_points_along_lines() - void add_point_as_influence_area(std::pair p, LayerIndex insert_layer, size_t dont_move_until, bool roof, bool skip_ovalisation) - { - bool to_bp = p.second == LineStatus::TO_BP || p.second == LineStatus::TO_BP_SAFE; - bool gracious = to_bp || p.second == LineStatus::TO_MODEL_GRACIOUS || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; - bool safe_radius = p.second == LineStatus::TO_BP_SAFE || p.second == LineStatus::TO_MODEL_GRACIOUS_SAFE; - if (! config.support_rests_on_model && ! to_bp) { - BOOST_LOG_TRIVIAL(warning) << "Tried to add an invalid support point"; - tree_supports_show_error("Unable to add tip. Some overhang may not be supported correctly."sv, true); - return; - } - Polygons circle{ m_base_circle }; - circle.front().translate(p.first); - { - Point hash_pos = p.first / ((config.min_radius + 1) / 10); - std::lock_guard critical_section_movebounds(m_mutex_movebounds); - if (!m_already_inserted[insert_layer].count(hash_pos)) { - // normalize the point a bit to also catch points which are so close that inserting it would achieve nothing - m_already_inserted[insert_layer].emplace(hash_pos); - static constexpr const size_t dtt = 0; - SupportElementState state; - state.target_height = insert_layer; - state.target_position = p.first; - state.next_position = p.first; - state.layer_idx = insert_layer; - state.effective_radius_height = dtt; - state.to_buildplate = to_bp; - state.distance_to_top = dtt; - state.result_on_layer = p.first; - assert(state.result_on_layer_is_set()); - state.increased_to_model_radius = 0; - state.to_model_gracious = gracious; - state.elephant_foot_increases = 0; - state.use_min_xy_dist = min_xy_dist; - state.supports_roof = roof; - state.dont_move_until = dont_move_until; - state.can_use_safe_radius = safe_radius; - state.missing_roof_layers = force_tip_to_roof ? dont_move_until : 0; - state.skip_ovalisation = skip_ovalisation; - move_bounds[insert_layer].emplace_back(state, std::move(circle)); - } - } - } - - // Outputs - std::vector &move_bounds; - SupportGeneratorLayerStorage &layer_storage; - SupportGeneratorLayersPtr &top_contacts; - SupportGeneratorLayersPtr &top_interfaces; - SupportGeneratorLayersPtr &top_base_interfaces; - - // Temps - static constexpr const auto m_base_radius = scaled(0.01); - const Polygon m_base_circle { make_circle(m_base_radius, SUPPORT_TREE_CIRCLE_RESOLUTION) }; - - // Mutexes, guards - std::mutex m_mutex_movebounds; - std::mutex m_mutex_layer_storage; - std::vector> m_already_inserted; -}; - // Called by generate_initial_areas(), used in parallel by multiple layers. // Produce // 1) Maximum num_support_roof_layers roof (top interface & contact) layers. @@ -1309,7 +1330,7 @@ void sample_overhang_area( // Configuration classes const TreeSupportMeshGroupSettings &mesh_group_settings, // Configuration & Output - InterfacePlacer &interface_placer) + RichInterfacePlacer &interface_placer) { // Assumption is that roof will support roof further up to avoid a lot of unnecessary branches. Each layer down it is checked whether the roof area // is still large enough to be a roof and aborted as soon as it is not. This part was already reworked a few times, and there could be an argument @@ -1447,17 +1468,11 @@ static void generate_initial_areas( const TreeSupportSettings &config, const std::vector &overhangs, std::vector &move_bounds, - SupportGeneratorLayersPtr &top_contacts, - SupportGeneratorLayersPtr &top_interfaces, - SupportGeneratorLayersPtr &top_base_interfaces, - SupportGeneratorLayerStorage &layer_storage, + InterfacePlacer &interface_placer, std::function throw_on_cancel) { using AvoidanceType = TreeModelVolumes::AvoidanceType; TreeSupportMeshGroupSettings mesh_group_settings(print_object); - SupportParameters support_params(print_object); - support_params.with_sheath = true; - support_params.support_density = 0; // To ensure z_distance_top_layers are left empty between the overhang (zeroth empty layer), the support has to be added z_distance_top_layers+1 layers below const size_t z_distance_delta = config.z_distance_top_layers + 1; @@ -1487,7 +1502,7 @@ static void generate_initial_areas( ; const size_t num_support_roof_layers = mesh_group_settings.support_roof_layers; const bool roof_enabled = num_support_roof_layers > 0; - const bool force_tip_to_roof = roof_enabled && sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area; + const bool force_tip_to_roof = roof_enabled && (interface_placer.support_parameters.soluble_interface || sqr(config.min_radius) * M_PI > mesh_group_settings.minimum_roof_area); // cap for how much layer below the overhang a new support point may be added, as other than with regular support every new inserted point // may cause extra material and time cost. Could also be an user setting or differently calculated. Idea is that if an overhang // does not turn valid in double the amount of layers a slope of support angle would take to travel xy_distance, nothing reasonable will come from it. @@ -1513,7 +1528,7 @@ static void generate_initial_areas( const size_t num_raft_layers = config.raft_layers.size(); const size_t first_support_layer = std::max(int(num_raft_layers) - int(z_distance_delta), 1); num_support_layers = size_t(std::max(0, int(print_object.layer_count()) + int(num_raft_layers) - int(z_distance_delta))); - raft_contact_layer_idx = generate_raft_contact(print_object, config, top_contacts, layer_storage); + raft_contact_layer_idx = generate_raft_contact(print_object, config, interface_placer); // Enumerate layers for which the support tips may be generated from overhangs above. raw_overhangs.reserve(num_support_layers - first_support_layer); for (size_t layer_idx = first_support_layer; layer_idx < num_support_layers; ++ layer_idx) @@ -1521,14 +1536,12 @@ static void generate_initial_areas( raw_overhangs.push_back({ layer_idx, &overhangs[overhang_idx] }); } - InterfacePlacer interface_placer{ print_object.slicing_parameters(), support_params, volumes, config, force_tip_to_roof, num_support_layers, - // Outputs - move_bounds, layer_storage, top_contacts, top_interfaces, top_base_interfaces }; + RichInterfacePlacer rich_interface_placer{ interface_placer, volumes, force_tip_to_roof, num_support_layers, move_bounds }; tbb::parallel_for(tbb::blocked_range(0, raw_overhangs.size()), - [&volumes, &config, &raw_overhangs, &mesh_group_settings, &support_params, + [&volumes, &config, &raw_overhangs, &mesh_group_settings, min_xy_dist, force_tip_to_roof, roof_enabled, num_support_roof_layers, extra_outset, circle_length_to_half_linewidth_change, connect_length, max_overhang_insert_lag, - &interface_placer, &throw_on_cancel](const tbb::blocked_range &range) { + &rich_interface_placer, &throw_on_cancel](const tbb::blocked_range &range) { for (size_t raw_overhang_idx = range.begin(); raw_overhang_idx < range.end(); ++ raw_overhang_idx) { size_t layer_idx = raw_overhangs[raw_overhang_idx].first; const Polygons &overhang_raw = *raw_overhangs[raw_overhang_idx].second; @@ -1620,7 +1633,7 @@ static void generate_initial_areas( LineInformations fresh_valid_points = convert_lines_to_internal(volumes, config, convert_internal_to_lines(split.second), layer_idx - lag_ctr); validate_range(fresh_valid_points); - interface_placer.add_points_along_lines(fresh_valid_points, (force_tip_to_roof && lag_ctr <= num_support_roof_layers) ? num_support_roof_layers : 0, layer_idx - lag_ctr, false, roof_enabled ? num_support_roof_layers : 0); + rich_interface_placer.add_points_along_lines(fresh_valid_points, (force_tip_to_roof && lag_ctr <= num_support_roof_layers) ? num_support_roof_layers : 0, layer_idx - lag_ctr, false, roof_enabled ? num_support_roof_layers : 0); } } #endif @@ -1638,7 +1651,7 @@ static void generate_initial_areas( //check_self_intersections(overhang_regular, "overhang_regular3"); for (ExPolygon &roof_part : union_ex(overhang_roofs)) { sample_overhang_area(to_polygons(std::move(roof_part)), true, layer_idx, num_support_roof_layers, connect_length, - mesh_group_settings, interface_placer); + mesh_group_settings, rich_interface_placer); throw_on_cancel(); } } @@ -1649,13 +1662,13 @@ static void generate_initial_areas( for (ExPolygon &support_part : union_ex(overhang_regular)) { sample_overhang_area(to_polygons(std::move(support_part)), false, layer_idx, num_support_roof_layers, connect_length, - mesh_group_settings, interface_placer); + mesh_group_settings, rich_interface_placer); throw_on_cancel(); } } }); - finalize_raft_contact(print_object, raft_contact_layer_idx, top_contacts, move_bounds); + finalize_raft_contact(print_object, raft_contact_layer_idx, interface_placer.top_contacts_mutable(), move_bounds); } static unsigned int move_inside(const Polygons &polygons, Point &from, int distance = 0, int64_t maxDist2 = std::numeric_limits::max()) @@ -4172,6 +4185,7 @@ static void draw_branches( // I/O: SupportGeneratorLayersPtr &bottom_contacts, SupportGeneratorLayersPtr &top_contacts, + InterfacePlacer &interface_placer, // Output: SupportGeneratorLayersPtr &intermediate_layers, @@ -4346,7 +4360,7 @@ static void draw_branches( mesh_slicing_params.mode = MeshSlicingParams::SlicingMode::Positive; tbb::parallel_for(tbb::blocked_range(0, trees.size(), 1), - [&trees, &volumes, &config, &slicing_params, &move_bounds, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { + [&trees, &volumes, &config, &slicing_params, &move_bounds, &interface_placer, &mesh_slicing_params, &throw_on_cancel](const tbb::blocked_range &range) { indexed_triangle_set partial_mesh; std::vector slice_z; std::vector bottom_contacts; @@ -4434,10 +4448,20 @@ static void draw_branches( *it_dst ++ = std::move(it_src->polygons); } } - if (branch.has_tip) { + +#if 0 + //FIXME branch.has_tip seems to not be reliable. + if (branch.has_tip && interface_placer.support_parameters.has_top_contacts) // Add top slices to top contacts / interfaces / base interfaces. - //slices; - } + for (int i = int(branch.path.size()) - 1; i >= 0; -- i) { + const SupportElement &el = *branch.path[i]; + if (el.state.missing_roof_layers == 0) + break; + //FIXME Move or not? + interface_placer.add_roof(std::move(slices[int(slices.size()) - i - 1]), el.state.layer_idx, + interface_placer.support_parameters.num_top_interface_layers + 1 - el.state.missing_roof_layers); + } +#endif } layer_begin += LayerIndex(num_empty); @@ -4619,34 +4643,44 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume std::vector overhangs = generate_overhangs(config, *print.get_object(processing.second.front()), throw_on_cancel); // ### Precalculate avoidances, collision etc. - + size_t num_support_layers = precalculate(print, overhangs, processing.first, processing.second, volumes, throw_on_cancel); + bool has_support = num_support_layers > 0; + num_support_layers = std::max(num_support_layers, config.raft_layers.size()); + + SupportParameters support_params(print_object); + support_params.with_sheath = true; + support_params.support_density = 0; + SupportGeneratorLayerStorage layer_storage; SupportGeneratorLayersPtr top_contacts; SupportGeneratorLayersPtr bottom_contacts; SupportGeneratorLayersPtr interface_layers; SupportGeneratorLayersPtr base_interface_layers; - SupportGeneratorLayersPtr intermediate_layers; + SupportGeneratorLayersPtr intermediate_layers(num_support_layers, nullptr); + if (support_params.has_top_contacts) + top_contacts.assign(num_support_layers, nullptr); + if (support_params.has_bottom_contacts) + bottom_contacts.assign(num_support_layers, nullptr); + if (support_params.has_interfaces()) + interface_layers.assign(num_support_layers, nullptr); + if (support_params.has_base_interfaces()) + base_interface_layers.assign(num_support_layers, nullptr); - SupportParameters support_params(print_object); - - if (size_t num_support_layers = precalculate(print, overhangs, processing.first, processing.second, volumes, throw_on_cancel); - num_support_layers > 0) { + InterfacePlacer interface_placer{ + print_object.slicing_parameters(), support_params, config, + // Outputs + layer_storage, top_contacts, interface_layers, base_interface_layers }; + if (has_support) { auto t_precalc = std::chrono::high_resolution_clock::now(); // value is the area where support may be placed. As this is calculated in CreateLayerPathing it is saved and reused in draw_areas std::vector move_bounds(num_support_layers); // ### Place tips of the support tree - bottom_contacts .assign(num_support_layers, nullptr); - top_contacts .assign(num_support_layers, nullptr); - interface_layers .assign(num_support_layers, nullptr); - base_interface_layers.assign(num_support_layers, nullptr); - intermediate_layers .assign(num_support_layers, nullptr); - for (size_t mesh_idx : processing.second) generate_initial_areas(*print.get_object(mesh_idx), volumes, config, overhangs, - move_bounds, top_contacts, interface_layers, base_interface_layers, layer_storage, throw_on_cancel); + move_bounds, interface_placer, throw_on_cancel); auto t_gen = std::chrono::high_resolution_clock::now(); #ifdef TREESUPPORT_DEBUG_SVG @@ -4679,7 +4713,7 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume assert(print_object.config().support_material_style == smsOrganic); draw_branches( *print.get_object(processing.second.front()), volumes, config, move_bounds, - bottom_contacts, top_contacts, intermediate_layers, layer_storage, + bottom_contacts, top_contacts, interface_placer, intermediate_layers, layer_storage, throw_on_cancel); } @@ -4713,12 +4747,9 @@ static void generate_support_areas(Print &print, const BuildVolume &build_volume // BOOST_LOG_TRIVIAL(error) << "Why ask questions when you already know the answer twice.\n (This is not a real bug, please dont report it.)"; move_bounds.clear(); - } else { - top_contacts.assign(config.raft_layers.size(), nullptr); - if (generate_raft_contact(print_object, config, top_contacts, layer_storage) < 0) - // No raft. - continue; - } + } else if (generate_raft_contact(print_object, config, interface_placer) < 0) + // No raft. + continue; // Produce the support G-code. // Used by both classic and tree supports. From 84db6356b3c7a27a5c1bd2762fa93a17ed9bbe00 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 9 May 2023 10:46:56 +0200 Subject: [PATCH 109/115] Organic Supports: support_tree_branch_diameter_double_wall to control when the 2nd wall kicks in. --- src/libslic3r/Preset.cpp | 3 +- src/libslic3r/PrintConfig.cpp | 12 +++++++ src/libslic3r/PrintConfig.hpp | 1 + src/libslic3r/PrintObject.cpp | 1 + src/libslic3r/Support/SupportCommon.cpp | 35 +++++++++++---------- src/libslic3r/Support/SupportParameters.cpp | 2 ++ src/libslic3r/Support/SupportParameters.hpp | 2 ++ src/slic3r/GUI/ConfigManipulation.cpp | 3 +- src/slic3r/GUI/Tab.cpp | 1 + 9 files changed, 41 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 23f1438c4..d20514bba 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -444,7 +444,8 @@ static std::vector s_Preset_print_options { "support_material_interface_pattern", "support_material_interface_spacing", "support_material_interface_contact_loops", "support_material_contact_distance", "support_material_bottom_contact_distance", "support_material_buildplate_only", - "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_top_rate", "support_tree_branch_distance", "support_tree_tip_diameter", + "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", "support_tree_branch_diameter_angle", "support_tree_branch_diameter_double_wall", + "support_tree_top_rate", "support_tree_branch_distance", "support_tree_tip_diameter", "dont_support_bridges", "thick_bridges", "notes", "complete_objects", "extruder_clearance_radius", "extruder_clearance_height", "gcode_comments", "gcode_label_objects", "output_filename_format", "post_process", "gcode_substitutions", "perimeter_extruder", "infill_extruder", "solid_infill_extruder", "support_material_extruder", "support_material_interface_extruder", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d48f18aa5..77120bfdf 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -2939,6 +2939,18 @@ void PrintConfigDef::init_fff_params() def->mode = comAdvanced; def->set_default_value(new ConfigOptionFloat(5)); + def = this->add("support_tree_branch_diameter_double_wall", coFloat); + def->label = L("Branch Diameter with double walls"); + def->category = L("Support material"); + // TRN PrintSettings: "Organic supports" > "Branch Diameter" + def->tooltip = L("Branches with area larger than the area of a circle of this diameter will be printed with double walls for stability. " + "Set this value to zero for no double walls."); + def->sidetext = L("mm"); + def->min = 0; + def->max = 100.f; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(3)); + // Tree Support Branch Distance // How far apart the branches need to be when they touch the model. Making this distance small will cause // the tree support to touch the model at more points, causing better overhang but making support harder to remove. diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index b9ca95a15..2a4b3258d 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -554,6 +554,7 @@ PRINT_CONFIG_CLASS_DEFINE( ((ConfigOptionFloat, support_tree_angle_slow)) ((ConfigOptionFloat, support_tree_branch_diameter)) ((ConfigOptionFloat, support_tree_branch_diameter_angle)) + ((ConfigOptionFloat, support_tree_branch_diameter_double_wall)) ((ConfigOptionPercent, support_tree_top_rate)) ((ConfigOptionFloat, support_tree_branch_distance)) ((ConfigOptionFloat, support_tree_tip_diameter)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index b43afd6be..1c37339a2 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -712,6 +712,7 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "support_tree_angle_slow" || opt_key == "support_tree_branch_diameter" || opt_key == "support_tree_branch_diameter_angle" + || opt_key == "support_tree_branch_diameter_double_wall" || opt_key == "support_tree_top_rate" || opt_key == "support_tree_branch_distance" || opt_key == "support_tree_tip_diameter" diff --git a/src/libslic3r/Support/SupportCommon.cpp b/src/libslic3r/Support/SupportCommon.cpp index c01e49bff..2035f9ea3 100644 --- a/src/libslic3r/Support/SupportCommon.cpp +++ b/src/libslic3r/Support/SupportCommon.cpp @@ -537,7 +537,8 @@ static Polylines draw_perimeters(const ExPolygon &expoly, double clip_length) static inline void tree_supports_generate_paths( ExtrusionEntitiesPtr &dst, const Polygons &polygons, - const Flow &flow) + const Flow &flow, + const SupportParameters &support_params) { // Offset expolygon inside, returns number of expolygons collected (0 or 1). // Vertices of output paths are marked with Z = source contour index of the expoly. @@ -634,21 +635,21 @@ static inline void tree_supports_generate_paths( ClipperLib_Z::Paths anchor_candidates; for (ExPolygon& expoly : closing_ex(polygons, float(SCALED_EPSILON), float(SCALED_EPSILON + 0.5 * flow.scaled_width()))) { std::unique_ptr eec; - double area = expoly.area(); - if (area > sqr(scaled(5.))) { - eec = std::make_unique(); - // Don't reoder internal / external loops of the same island, always start with the internal loop. - eec->no_sort = true; - // Make the tree branch stable by adding another perimeter. - ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width()); - if (level2.size() == 1) { - Polylines polylines; - extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), - // Disable reversal of the path, always start with the anchor, always print CCW. - false); - expoly = level2.front(); + if (support_params.tree_branch_diameter_double_wall_area_scaled > 0) + if (double area = expoly.area(); area > support_params.tree_branch_diameter_double_wall_area_scaled) { + eec = std::make_unique(); + // Don't reoder internal / external loops of the same island, always start with the internal loop. + eec->no_sort = true; + // Make the tree branch stable by adding another perimeter. + ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width()); + if (level2.size() == 1) { + Polylines polylines; + extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), ExtrusionRole::SupportMaterial, flow.mm3_per_mm(), flow.width(), flow.height(), + // Disable reversal of the path, always start with the anchor, always print CCW. + false); + expoly = level2.front(); + } } - } // Try to produce one more perimeter to place the seam anchor. // First genrate a 2nd perimeter loop as a source for anchor candidates. @@ -1531,7 +1532,7 @@ void generate_support_toolpaths( support_params.with_sheath, false); } if (! tree_polygons.empty()) - tree_supports_generate_paths(support_layer.support_fills.entities, tree_polygons, flow); + tree_supports_generate_paths(support_layer.support_fills.entities, tree_polygons, flow, support_params); } Fill *filler = filler_interface.get(); @@ -1790,7 +1791,7 @@ void generate_support_toolpaths( sheath = true; no_sort = true; } else if (config.support_material_style == SupportMaterialStyle::smsOrganic) { - tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow); + tree_supports_generate_paths(base_layer.extrusions, base_layer.polygons_to_extrude(), flow, support_params); done = true; } if (! done) diff --git a/src/libslic3r/Support/SupportParameters.cpp b/src/libslic3r/Support/SupportParameters.cpp index 531e8dcaf..09eca9610 100644 --- a/src/libslic3r/Support/SupportParameters.cpp +++ b/src/libslic3r/Support/SupportParameters.cpp @@ -137,6 +137,8 @@ SupportParameters::SupportParameters(const PrintObject &object) assert(slicing_params.interface_raft_layers == 0); assert(slicing_params.raft_layers() == 0); } + + this->tree_branch_diameter_double_wall_area_scaled = 0.25 * sqr(scaled(object_config.support_tree_branch_diameter_double_wall.value)) * M_PI; } } // namespace Slic3r diff --git a/src/libslic3r/Support/SupportParameters.hpp b/src/libslic3r/Support/SupportParameters.hpp index be38e9650..8a63d9f3f 100644 --- a/src/libslic3r/Support/SupportParameters.hpp +++ b/src/libslic3r/Support/SupportParameters.hpp @@ -77,6 +77,8 @@ struct SupportParameters { InfillPattern contact_fill_pattern; // Shall the sparse (base) layers be printed with a single perimeter line (sheath) for robustness? bool with_sheath; + // Branches of organic supports with area larger than this threshold will be extruded with double lines. + double tree_branch_diameter_double_wall_area_scaled; float raft_angle_1st_layer; float raft_angle_base; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 4c5b0fd8e..f645e8a0d 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -291,7 +291,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) (config->opt_bool("support_material") || config->opt_int("support_material_enforce_layers") > 0); for (const std::string& key : { "support_tree_angle", "support_tree_angle_slow", "support_tree_branch_diameter", - "support_tree_branch_diameter_angle", "support_tree_tip_diameter", "support_tree_branch_distance", "support_tree_top_rate" }) + "support_tree_branch_diameter_angle", "support_tree_branch_diameter_double_wall", + "support_tree_tip_diameter", "support_tree_branch_distance", "support_tree_top_rate" }) toggle_field(key, has_organic_supports); for (auto el : { "support_material_bottom_interface_layers", "support_material_interface_spacing", "support_material_interface_extruder", diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 99843e541..2cf4969cb 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1536,6 +1536,7 @@ void TabPrint::build() optgroup->append_single_option_line("support_tree_angle_slow", category_path + "tree_angle_slow"); optgroup->append_single_option_line("support_tree_branch_diameter", category_path + "tree_branch_diameter"); optgroup->append_single_option_line("support_tree_branch_diameter_angle", category_path + "tree_branch_diameter_angle"); + optgroup->append_single_option_line("support_tree_branch_diameter_double_wall", category_path + "tree_branch_diameter_double_wall"); optgroup->append_single_option_line("support_tree_tip_diameter", category_path + "tree_tip_diameter"); optgroup->append_single_option_line("support_tree_branch_distance", category_path + "tree_branch_distance"); optgroup->append_single_option_line("support_tree_top_rate", category_path + "tree_top_rate"); From c9f449bcb2df5a9cce6dd7c97b78fc585e6bc757 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 9 May 2023 11:14:36 +0200 Subject: [PATCH 110/115] PlaceholderParser: fixed reporting of x value outside of the interpolated table. --- src/libslic3r/PlaceholderParser.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 1dbf4120e..348825106 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -1644,9 +1644,9 @@ namespace client } if (! evaluated) { // Clamp x into the table range with EPSILON. - if (x > table.table.front().x - EPSILON) + if (double x0 = table.table.front().x; x > x0 - EPSILON && x < x0) out.set_d(table.table.front().y); - else if (x < table.table.back().x + EPSILON) + else if (double x1 = table.table.back().x; x > x1 && x < x1 + EPSILON) out.set_d(table.table.back().y); else // The value is really outside the table range. From 3848a8eda23d532cdbe4b226e6f4a0aecc528c35 Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Wed, 10 May 2023 09:02:40 +0200 Subject: [PATCH 111/115] Fixed crash after 'vb_organic_interfaces' with zero top interfaces. --- src/libslic3r/Support/TreeSupport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 0eb009c77..d53b3e785 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -4558,7 +4558,7 @@ static void draw_branches( } // Subtract top contact layer polygons from support base. - SupportGeneratorLayer *top_contact_layer = top_contacts[layer_idx]; + SupportGeneratorLayer *top_contact_layer = top_contacts.empty() ? nullptr : top_contacts[layer_idx]; if (top_contact_layer && ! top_contact_layer->polygons.empty() && ! base_layer_polygons.empty()) { base_layer_polygons = diff(base_layer_polygons, top_contact_layer->polygons); if (! bottom_contact_polygons.empty()) From 2e3b5c2bbc0e31c1737a7824051002afa92c92d7 Mon Sep 17 00:00:00 2001 From: David Kocik Date: Thu, 20 Apr 2023 17:27:09 +0200 Subject: [PATCH 112/115] Fix of backward compability issue Added recommended version checks to ConfigWizard and PresetUpdater --- src/slic3r/GUI/ConfigWizard.cpp | 41 +++++++++++++++- src/slic3r/Utils/PresetUpdater.cpp | 79 +++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 25 deletions(-) diff --git a/src/slic3r/GUI/ConfigWizard.cpp b/src/slic3r/GUI/ConfigWizard.cpp index 11b116a05..866d5a817 100644 --- a/src/slic3r/GUI/ConfigWizard.cpp +++ b/src/slic3r/GUI/ConfigWizard.cpp @@ -57,6 +57,7 @@ #include "UnsavedChangesDialog.hpp" #include "slic3r/Utils/AppUpdater.hpp" #include "slic3r/GUI/I18N.hpp" +#include "slic3r/Config/Version.hpp" #if defined(__linux__) && defined(__WXGTK3__) #define wxLinux_gtk3 true @@ -118,7 +119,7 @@ BundleMap BundleMap::load() const auto vendor_dir = (boost::filesystem::path(Slic3r::data_dir()) / "vendor").make_preferred(); const auto archive_dir = (boost::filesystem::path(Slic3r::data_dir()) / "cache" / "vendor").make_preferred(); const auto rsrc_vendor_dir = (boost::filesystem::path(resources_dir()) / "profiles").make_preferred(); - + const auto cache_dir = boost::filesystem::path(Slic3r::data_dir()) / "cache"; // for Index // Load Prusa bundle from the datadir/vendor directory or from datadir/cache/vendor (archive) or from resources/profiles. auto prusa_bundle_path = (vendor_dir / PresetBundle::PRUSA_BUNDLE).replace_extension(".ini"); BundleLocation prusa_bundle_loc = BundleLocation::IN_VENDOR; @@ -138,7 +139,7 @@ BundleMap BundleMap::load() // Load the other bundles in the datadir/vendor directory // and then additionally from datadir/cache/vendor (archive) and resources/profiles. - // Should we concider case where archive has older profiles than resources (shouldnt happen)? + // Should we concider case where archive has older profiles than resources (shouldnt happen)? -> YES, it happens during re-configuration when running older PS after newer version typedef std::pair DirData; std::vector dir_list { {vendor_dir, BundleLocation::IN_VENDOR}, {archive_dir, BundleLocation::IN_ARCHIVE}, {rsrc_vendor_dir, BundleLocation::IN_RESOURCES} }; for ( auto dir : dir_list) { @@ -151,6 +152,42 @@ BundleMap BundleMap::load() // Don't load this bundle if we've already loaded it. if (res.find(id) != res.end()) { continue; } + // Fresh index should be in archive_dir, otherwise look for it in cache + fs::path idx_path (archive_dir / (id + ".idx")); + if (!boost::filesystem::exists(idx_path)) { + BOOST_LOG_TRIVIAL(warning) << format("Missing index %1% when loading bundle %2%.", idx_path.string(), id); + idx_path = fs::path(cache_dir / (id + ".idx")); + } + if (!boost::filesystem::exists(idx_path)) { + BOOST_LOG_TRIVIAL(error) << format("Could not load bundle %1% due to missing index %1%.", id, idx_path.string()); + continue; + } + Slic3r::GUI::Config::Index index; + try { + index.load(idx_path); + } + catch (const std::exception& /* err */) { + BOOST_LOG_TRIVIAL(error) << format("Could not load bundle %1% due to invalid index %1%.", id, idx_path.string()); + continue; + } + const auto recommended_it = index.recommended(); + if (recommended_it == index.end()) { + BOOST_LOG_TRIVIAL(error) << format("Could not load bundle %1% due to no recommended version in index %2%.", id, idx_path.string()); + continue; + } + const auto recommended = recommended_it->config_version; + VendorProfile vp; + try { + vp = VendorProfile::from_ini(dir_entry, true); + } + catch (const std::exception& e) { + BOOST_LOG_TRIVIAL(error) << format("Could not load bundle %1% due to corrupted profile file %2%. Message: %3%", id, dir_entry.path().string(), e.what()); + continue; + } + // Don't load + if (vp.config_version > recommended) + continue; + Bundle bundle; if (bundle.load(dir_entry.path(), dir.second)) res.emplace(std::move(id), std::move(bundle)); diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index 028c7ce0a..e75b5420a 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -1320,7 +1320,35 @@ bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vectorcache_path / idx_path.filename()); + } + if (!boost::filesystem::exists(idx_path)) { + std::string msg = GUI::format(_L("Couldn't locate index file for vendor %1% when performing updates. The profile will not be installed."), bundle); + BOOST_LOG_TRIVIAL(error) << msg; + GUI::show_error(nullptr, msg); + continue; + } + Slic3r::GUI::Config::Index index; + try { + index.load(idx_path); + } + catch (const std::exception& /* err */) { + std::string msg = GUI::format(_L("Couldn't load index file for vendor %1% when performing updates. The profile will not be installed. Reason: Corrupted index file %2%."), bundle, idx_path.string()); + BOOST_LOG_TRIVIAL(error) << msg; + GUI::show_error(nullptr, msg); + continue; + } + const auto recommended_it = index.recommended(); + const auto recommended = recommended_it->config_version; + if (is_in_cache_vendor) { Semver version_cache = Semver::zero(); try { @@ -1329,13 +1357,11 @@ bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector recommended) + version_cache = Semver::zero(); + Semver version_rsrc = Semver::zero(); try { if (is_in_rsrc) { @@ -1345,26 +1371,33 @@ bool PresetUpdater::install_bundles_rsrc_or_cache_vendor(std::vector recommended) + version_rsrc = Semver::zero(); - if (!is_in_rsrc || version_cache > version_rsrc) { - // in case we are installing from cache / vendor. we should also copy index to cache - // This needs to be done now bcs the current one would be missing this version on next start - // dk: Should we copy it to vendor dir too? - auto path_idx_cache_vendor(path_in_cache_vendor); - path_idx_cache_vendor.replace_extension(".idx"); - auto path_idx_cache = (p->cache_path / bundle).replace_extension(".idx"); - // DK: do this during perform_updates() too? - if (fs::exists(path_idx_cache_vendor)) - copy_file_fix(path_idx_cache_vendor, path_idx_cache); - else // Should we dialog this? - BOOST_LOG_TRIVIAL(error) << GUI::format(_L("Couldn't locate idx file %1% when performing updates."), path_idx_cache_vendor.string()); + if (version_cache == Semver::zero() && version_rsrc == Semver::zero()) { + std::string msg = GUI::format(_L("Couldn't open profile file for vendor %1% when performing updates. The profile will not be installed. This installation might be corrupted."), bundle); + BOOST_LOG_TRIVIAL(error) << msg; + GUI::show_error(nullptr, msg); + continue; + } else if (version_cache == Semver::zero()) { + // cache vendor cannot be used, use resources + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + } else if (version_rsrc == Semver::zero()) { + // resources cannto be used, use cache vendor + updates.updates.emplace_back(std::move(path_in_cache_vendor), std::move(path_in_vendors), Version(), "", ""); + } else if (version_cache > version_rsrc) { + // in case we are installing from cache / vendor. we should also copy index to cache + // This needs to be done now bcs the current one would be missing this version on the next start + auto path_idx_cache = (p->cache_path / bundle).replace_extension(".idx"); + if (idx_path != path_idx_cache) + copy_file_fix(idx_path, path_idx_cache); updates.updates.emplace_back(std::move(path_in_cache_vendor), std::move(path_in_vendors), Version(), "", ""); - } else { - if (is_in_rsrc) - updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); + updates.updates.emplace_back(std::move(path_in_rsrc), std::move(path_in_vendors), Version(), "", ""); } } else { if (! is_in_rsrc) { From f346b42886d394c8a7b1388acf957ba31397e4ed Mon Sep 17 00:00:00 2001 From: David Kocik Date: Wed, 10 May 2023 11:17:10 +0200 Subject: [PATCH 113/115] Commented Part of config update algorythm that searches in snapshots. --- src/slic3r/Utils/PresetUpdater.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/slic3r/Utils/PresetUpdater.cpp b/src/slic3r/Utils/PresetUpdater.cpp index e75b5420a..995891db9 100644 --- a/src/slic3r/Utils/PresetUpdater.cpp +++ b/src/slic3r/Utils/PresetUpdater.cpp @@ -961,7 +961,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version BOOST_LOG_TRIVIAL(error) << format("Cannot load the installed index at `%1%`: %2%", bundle_path_idx, err.what()); } } - +#if 0 // Check if the update is already present in a snapshot if(!current_not_supported) { @@ -974,7 +974,7 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version continue; } } - +#endif // 0 updates.updates.emplace_back(std::move(new_update)); // 'Install' the index in the vendor directory. This is used to memoize // offered updates and to not offer the same update again if it was cancelled by the user. From d4ad9deb94ad239bf18f95b6188fefabeff83ad8 Mon Sep 17 00:00:00 2001 From: tamasmeszaros Date: Wed, 5 Apr 2023 16:58:54 +0200 Subject: [PATCH 114/115] Archive format registration refactored Fix tests --- src/libslic3r/CMakeLists.txt | 2 + src/libslic3r/Format/AnycubicSLA.hpp | 55 +++---- src/libslic3r/Format/SL1.cpp | 2 + .../Format/SLAArchiveFormatRegistry.cpp | 147 ++++++++++++++++++ .../Format/SLAArchiveFormatRegistry.hpp | 71 +++++++++ src/libslic3r/Format/SLAArchiveReader.cpp | 94 +++-------- src/libslic3r/Format/SLAArchiveReader.hpp | 9 -- src/libslic3r/Format/SLAArchiveWriter.cpp | 71 +-------- src/libslic3r/Format/SLAArchiveWriter.hpp | 6 - src/slic3r/GUI/Jobs/SLAImportDialog.hpp | 14 +- .../sla_print/sla_archive_readwrite_tests.cpp | 19 ++- 11 files changed, 285 insertions(+), 205 deletions(-) create mode 100644 src/libslic3r/Format/SLAArchiveFormatRegistry.cpp create mode 100644 src/libslic3r/Format/SLAArchiveFormatRegistry.hpp diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d44bfcde0..297d2e3ff 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -131,6 +131,8 @@ set(SLIC3R_SOURCES Format/AnycubicSLA.cpp Format/STEP.hpp Format/STEP.cpp + Format/SLAArchiveFormatRegistry.hpp + Format/SLAArchiveFormatRegistry.cpp GCode/ThumbnailData.cpp GCode/ThumbnailData.hpp GCode/Thumbnails.cpp diff --git a/src/libslic3r/Format/AnycubicSLA.hpp b/src/libslic3r/Format/AnycubicSLA.hpp index 46eb68d00..d1f8adf8a 100644 --- a/src/libslic3r/Format/AnycubicSLA.hpp +++ b/src/libslic3r/Format/AnycubicSLA.hpp @@ -4,44 +4,14 @@ #include #include "SLAArchiveWriter.hpp" +#include "SLAArchiveFormatRegistry.hpp" #include "libslic3r/PrintConfig.hpp" -#define ANYCUBIC_SLA_FORMAT_VERSION_1 1 -#define ANYCUBIC_SLA_FORMAT_VERSION_515 515 -#define ANYCUBIC_SLA_FORMAT_VERSION_516 516 -#define ANYCUBIC_SLA_FORMAT_VERSION_517 517 - -#define ANYCUBIC_SLA_FORMAT_VERSIONED(FILEFORMAT, NAME, VERSION) \ - { FILEFORMAT, { FILEFORMAT, [] (const auto &cfg) { return std::make_unique(cfg, VERSION); } } } - -#define ANYCUBIC_SLA_FORMAT(FILEFORMAT, NAME) \ - ANYCUBIC_SLA_FORMAT_VERSIONED(FILEFORMAT, NAME, ANYCUBIC_SLA_FORMAT_VERSION_1) - -/** - // Supports only ANYCUBIC_SLA_VERSION_1 - ANYCUBIC_SLA_FORMAT_VERSIONED("pws", "Photon / Photon S", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("pw0", "Photon Zero", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("pwx", "Photon X", ANYCUBIC_SLA_VERSION_1), - - // Supports ANYCUBIC_SLA_VERSION_1 and ANYCUBIC_SLA_VERSION_515 - ANYCUBIC_SLA_FORMAT_VERSIONED("pwmo", "Photon Mono", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("pwms", "Photon Mono SE", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("dlp", "Photon Ultra", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("pwmx", "Photon Mono X", ANYCUBIC_SLA_VERSION_1), - ANYCUBIC_SLA_FORMAT_VERSIONED("pmsq", "Photon Mono SQ", ANYCUBIC_SLA_VERSION_1), - - // Supports ANYCUBIC_SLA_VERSION_515 and ANYCUBIC_SLA_VERSION_516 - ANYCUBIC_SLA_FORMAT_VERSIONED("pwma", "Photon Mono 4K", ANYCUBIC_SLA_VERSION_515), - ANYCUBIC_SLA_FORMAT_VERSIONED("pm3", "Photon M3", ANYCUBIC_SLA_VERSION_515), - ANYCUBIC_SLA_FORMAT_VERSIONED("pm3m", "Photon M3 Max", ANYCUBIC_SLA_VERSION_515), - - // Supports NYCUBIC_SLA_VERSION_515 and ANYCUBIC_SLA_VERSION_516 and ANYCUBIC_SLA_VERSION_517 - ANYCUBIC_SLA_FORMAT_VERSIONED("pwmb", "Photon Mono X 6K / Photon M3 Plus", ANYCUBIC_SLA_VERSION_515), - ANYCUBIC_SLA_FORMAT_VERSIONED("dl2p", "Photon Photon D2", ANYCUBIC_SLA_VERSION_515), - ANYCUBIC_SLA_FORMAT_VERSIONED("pmx2", "Photon Mono X2", ANYCUBIC_SLA_VERSION_515), - ANYCUBIC_SLA_FORMAT_VERSIONED("pm3r", "Photon M3 Premium", ANYCUBIC_SLA_VERSION_515), -*/ +constexpr uint16_t ANYCUBIC_SLA_FORMAT_VERSION_1 = 1; +constexpr uint16_t ANYCUBIC_SLA_FORMAT_VERSION_515 = 515; +constexpr uint16_t ANYCUBIC_SLA_FORMAT_VERSION_516 = 516; +constexpr uint16_t ANYCUBIC_SLA_FORMAT_VERSION_517 = 517; namespace Slic3r { @@ -75,6 +45,21 @@ public: const std::string &projectname = "") override; }; +inline Slic3r::ArchiveEntry anycubic_sla_format_versioned(const char *fileformat, const char *desc, uint16_t version) +{ + Slic3r::ArchiveEntry entry(fileformat); + + entry.desc = desc; + entry.ext = fileformat; + entry.wrfactoryfn = [version] (const auto &cfg) { return std::make_unique(cfg, version); }; + + return entry; +} + +inline Slic3r::ArchiveEntry anycubic_sla_format(const char *fileformat, const char *desc) +{ + return anycubic_sla_format_versioned(fileformat, desc, ANYCUBIC_SLA_FORMAT_VERSION_1); +} } // namespace Slic3r::sla diff --git a/src/libslic3r/Format/SL1.cpp b/src/libslic3r/Format/SL1.cpp index 4a5a25b08..e9fc058e8 100644 --- a/src/libslic3r/Format/SL1.cpp +++ b/src/libslic3r/Format/SL1.cpp @@ -17,6 +17,7 @@ #include "libslic3r/GCode/ThumbnailData.hpp" #include "SLAArchiveReader.hpp" +#include "SLAArchiveFormatRegistry.hpp" #include "ZipperArchiveImport.hpp" #include "libslic3r/MarchingSquares.hpp" @@ -26,6 +27,7 @@ #include "libslic3r/SLA/RasterBase.hpp" + #include #include #include diff --git a/src/libslic3r/Format/SLAArchiveFormatRegistry.cpp b/src/libslic3r/Format/SLAArchiveFormatRegistry.cpp new file mode 100644 index 000000000..5c40a5c51 --- /dev/null +++ b/src/libslic3r/Format/SLAArchiveFormatRegistry.cpp @@ -0,0 +1,147 @@ +#include +#include +#include + +#include "SL1.hpp" +#include "SL1_SVG.hpp" +#include "AnycubicSLA.hpp" + +#include "SLAArchiveFormatRegistry.hpp" + +namespace Slic3r { + +static std::mutex arch_mtx; + +class Registry { + static std::unique_ptr registry; + + std::set entries; +public: + + Registry () + { + entries = { + { + "SL1", // id + L("SL1 archive format"), // description + "sl1", // main extension + {"sl1s", "zip"}, // extension aliases + + // Writer factory + [] (const auto &cfg) { return std::make_unique(cfg); }, + + // Reader factory + [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { + return std::make_unique(fname, quality, progr); + } + }, + { + "SL1SVG", + L("SL1SVG archive files"), + "sl1_svg", + {}, + [] (const auto &cfg) { return std::make_unique(cfg); }, + [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { + return std::make_unique(fname, quality, progr); + } + }, + { + "SL2", + "", + "sl1_svg", + {}, + [] (const auto &cfg) { return std::make_unique(cfg); }, + nullptr + }, + anycubic_sla_format("pwmo", "Photon Mono"), + anycubic_sla_format("pwmx", "Photon Mono X"), + anycubic_sla_format("pwms", "Photon Mono SE"), + + /** + // Supports only ANYCUBIC_SLA_VERSION_1 + anycubic_sla_format_versioned("pws", "Photon / Photon S", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("pw0", "Photon Zero", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("pwx", "Photon X", ANYCUBIC_SLA_VERSION_1), + + // Supports ANYCUBIC_SLA_VERSION_1 and ANYCUBIC_SLA_VERSION_515 + anycubic_sla_format_versioned("pwmo", "Photon Mono", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("pwms", "Photon Mono SE", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("dlp", "Photon Ultra", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("pwmx", "Photon Mono X", ANYCUBIC_SLA_VERSION_1), + anycubic_sla_format_versioned("pmsq", "Photon Mono SQ", ANYCUBIC_SLA_VERSION_1), + + // Supports ANYCUBIC_SLA_VERSION_515 and ANYCUBIC_SLA_VERSION_516 + anycubic_sla_format_versioned("pwma", "Photon Mono 4K", ANYCUBIC_SLA_VERSION_515), + anycubic_sla_format_versioned("pm3", "Photon M3", ANYCUBIC_SLA_VERSION_515), + anycubic_sla_format_versioned("pm3m", "Photon M3 Max", ANYCUBIC_SLA_VERSION_515), + + // Supports NYCUBIC_SLA_VERSION_515 and ANYCUBIC_SLA_VERSION_516 and ANYCUBIC_SLA_VERSION_517 + anycubic_sla_format_versioned("pwmb", "Photon Mono X 6K / Photon M3 Plus", ANYCUBIC_SLA_VERSION_515), + anycubic_sla_format_versioned("dl2p", "Photon Photon D2", ANYCUBIC_SLA_VERSION_515), + anycubic_sla_format_versioned("pmx2", "Photon Mono X2", ANYCUBIC_SLA_VERSION_515), + anycubic_sla_format_versioned("pm3r", "Photon M3 Premium", ANYCUBIC_SLA_VERSION_515), + */ + }; + } + + static Registry& get_instance() + { + if (!registry) + registry = std::make_unique(); + + return *registry; + } + + static std::set& get() + { + return get_instance().entries; + } + + std::set& get_entries() { return entries; } +}; + +std::unique_ptr Registry::registry = nullptr; + +std::set registered_sla_archives() +{ + std::lock_guard lk{arch_mtx}; + + return Registry::get(); +} + +std::vector get_extensions(const ArchiveEntry &entry) +{ + auto ret = reserve_vector(entry.ext_aliases.size() + 1); + + ret.emplace_back(entry.ext); + for (const char *alias : entry.ext_aliases) + ret.emplace_back(alias); + + return ret; +} + +ArchiveWriterFactory get_writer_factory(const char *formatid) +{ + std::lock_guard lk{arch_mtx}; + + ArchiveWriterFactory ret; + auto entry = Registry::get().find(ArchiveEntry{formatid}); + if (entry != Registry::get().end()) + ret = entry->wrfactoryfn; + + return ret; +} + +ArchiveReaderFactory get_reader_factory(const char *formatid) +{ + std::lock_guard lk{arch_mtx}; + + ArchiveReaderFactory ret; + auto entry = Registry::get().find(ArchiveEntry{formatid}); + if (entry != Registry::get().end()) + ret = entry->rdfactoryfn; + + return ret; +} + +} // namespace Slic3r::sla diff --git a/src/libslic3r/Format/SLAArchiveFormatRegistry.hpp b/src/libslic3r/Format/SLAArchiveFormatRegistry.hpp new file mode 100644 index 000000000..fb1a18ca5 --- /dev/null +++ b/src/libslic3r/Format/SLAArchiveFormatRegistry.hpp @@ -0,0 +1,71 @@ +#ifndef SLA_ARCHIVE_FORMAT_REGISTRY_HPP +#define SLA_ARCHIVE_FORMAT_REGISTRY_HPP + +#include "SLAArchiveWriter.hpp" +#include "SLAArchiveReader.hpp" +#include + +namespace Slic3r { + +// Factory function that returns an implementation of SLAArchiveWriter given +// a printer configuration. +using ArchiveWriterFactory = std::function< + std::unique_ptr(const SLAPrinterConfig &) +>; + +// Factory function that returns an implementation of SLAArchiveReader +using ArchiveReaderFactory = std::function< + std::unique_ptr(const std::string &fname, + SLAImportQuality quality, + const ProgrFn & progr) +>; + +struct ArchiveEntry { + // Main ID for the format, for internal unique identification + const char *id; + + // Generic description (usable in GUI) about an archive format. Should only + // be marked for localization (macro L). + const char *desc = ""; + + // Main extension of the format. + const char *ext = "zip"; + + ArchiveWriterFactory wrfactoryfn; + ArchiveReaderFactory rdfactoryfn; + + // Secondary, alias extensions + std::vector ext_aliases; + + explicit ArchiveEntry(const char *formatid) : id{formatid} {} + + ArchiveEntry(const char *formatid, + const char *description, + const char *extension, + std::initializer_list extaliases, + const ArchiveWriterFactory &wrfn, + const ArchiveReaderFactory &rdfn) + : id{formatid} + , desc{description} + , ext{extension} + , ext_aliases{extaliases} + , wrfactoryfn{wrfn} + , rdfactoryfn{rdfn} + {} + + bool operator <(const ArchiveEntry &other) const + { + return std::strcmp(id, other.id) < 0; + } +}; + +std::vector get_extensions(const ArchiveEntry &entry); + +std::set registered_sla_archives(); + +ArchiveWriterFactory get_writer_factory(const char *formatid); +ArchiveReaderFactory get_reader_factory(const char *formatid); + +} // namespace Slic3r + +#endif // ARCHIVEREGISTRY_HPP diff --git a/src/libslic3r/Format/SLAArchiveReader.cpp b/src/libslic3r/Format/SLAArchiveReader.cpp index b931ea0e4..c8a15bc5a 100644 --- a/src/libslic3r/Format/SLAArchiveReader.cpp +++ b/src/libslic3r/Format/SLAArchiveReader.cpp @@ -8,44 +8,13 @@ #include #include +#include "SLAArchiveFormatRegistry.hpp" #include #include namespace Slic3r { -namespace { - -// Factory function that returns an implementation of SLAArchiveReader. -using ArchiveFactory = std::function< - std::unique_ptr(const std::string &fname, - SLAImportQuality quality, - const ProgrFn & progr)>; - -// Entry in the global registry of readable archive formats. -struct ArchiveEntry { - const char *descr; - std::vector extensions; - ArchiveFactory factoryfn; -}; - -// This is where the readable archive formats are registered. -static const std::map REGISTERED_ARCHIVES { - { - "SL1", - { L("SL1 / SL1S archive files"), {"sl1", "sl1s", "zip"}, - [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { return std::make_unique(fname, quality, progr); } } - }, - { - "SL1SVG", - { L("SL1SVG archive files"), {"sl1_svg"/*, "zip"*/}, // also a zip but unnecessary hassle to implement single extension for multiple archives - [] (const std::string &fname, SLAImportQuality quality, const ProgrFn &progr) { return std::make_unique(fname, quality, progr); }} - }, - // TODO: pwmx and future others. -}; - -} // namespace - std::unique_ptr SLAArchiveReader::create( const std::string &fname, const std::string &format_id, @@ -64,11 +33,13 @@ std::unique_ptr SLAArchiveReader::create( std::unique_ptr ret; - auto arch_from = REGISTERED_ARCHIVES.begin(); - auto arch_to = REGISTERED_ARCHIVES.end(); + auto registry = registered_sla_archives(); - auto arch_it = REGISTERED_ARCHIVES.find(format_id); - if (arch_it != REGISTERED_ARCHIVES.end()) { + auto arch_from = registry.begin(); + auto arch_to = registry.end(); + + auto arch_it = registry.find(ArchiveEntry{format_id.c_str()}); + if (arch_it != registry.end()) { arch_from = arch_it; arch_to = arch_it; } @@ -77,52 +48,23 @@ std::unique_ptr SLAArchiveReader::create( if (ext.front() == '.') ext.erase(ext.begin()); - auto extcmp = [&ext](const auto &e) { return e == ext; }; - - for (auto it = arch_from; it != arch_to; ++it) { - const auto &[format_id, entry] = *it; - if (std::any_of(entry.extensions.begin(), entry.extensions.end(), extcmp)) - ret = entry.factoryfn(fname, quality, progr); + for (auto it = arch_from; !ret && it != arch_to; ++it) { + const auto &entry = *it; + if (entry.rdfactoryfn) { + auto extensions = get_extensions(entry); + for (const std::string& supportedext : extensions) { + if (ext == supportedext) { + ret = entry.rdfactoryfn(fname, quality, progr); + break; + } + } + } } } return ret; } -const std::vector &SLAArchiveReader::registered_archives() -{ - static std::vector archnames; - - if (archnames.empty()) { - archnames.reserve(REGISTERED_ARCHIVES.size()); - - for (auto &[name, _] : REGISTERED_ARCHIVES) - archnames.emplace_back(name.c_str()); - } - - return archnames; -} - -std::vector SLAArchiveReader::get_extensions(const char *archtype) -{ - auto it = REGISTERED_ARCHIVES.find(archtype); - - if (it != REGISTERED_ARCHIVES.end()) - return it->second.extensions; - - return {}; -} - -const char *SLAArchiveReader::get_description(const char *archtype) -{ - auto it = REGISTERED_ARCHIVES.find(archtype); - - if (it != REGISTERED_ARCHIVES.end()) - return it->second.descr; - - return nullptr; -} - struct SliceParams { double layerh = 0., initial_layerh = 0.; }; static SliceParams get_slice_params(const DynamicPrintConfig &cfg) diff --git a/src/libslic3r/Format/SLAArchiveReader.hpp b/src/libslic3r/Format/SLAArchiveReader.hpp index e7a99b043..df93ba1ba 100644 --- a/src/libslic3r/Format/SLAArchiveReader.hpp +++ b/src/libslic3r/Format/SLAArchiveReader.hpp @@ -47,15 +47,6 @@ public: const std::string &format_id, SLAImportQuality quality = SLAImportQuality::Balanced, const ProgrFn &progr = [](int) { return false; }); - - // Get the names of currently known archive reader implementations - static const std::vector & registered_archives(); - - // Get the understood file extensions belonging to an archive format - static std::vector get_extensions(const char *archtype); - - // Generic description (usable in GUI) about an archive format - static const char * get_description(const char *archtype); }; // Raised in import_sla_archive when a nullptr reader is returned by diff --git a/src/libslic3r/Format/SLAArchiveWriter.cpp b/src/libslic3r/Format/SLAArchiveWriter.cpp index 7546d7c46..5d3cee7cf 100644 --- a/src/libslic3r/Format/SLAArchiveWriter.cpp +++ b/src/libslic3r/Format/SLAArchiveWriter.cpp @@ -1,77 +1,18 @@ #include "SLAArchiveWriter.hpp" - -#include "SL1.hpp" -#include "SL1_SVG.hpp" -#include "AnycubicSLA.hpp" - -#include "libslic3r/libslic3r.h" - -#include -#include -#include -#include +#include "SLAArchiveFormatRegistry.hpp" namespace Slic3r { -using ArchiveFactory = std::function(const SLAPrinterConfig&)>; - -struct ArchiveEntry { - const char *ext; - ArchiveFactory factoryfn; -}; - -static const std::map REGISTERED_ARCHIVES { - { - "SL1", - { "sl1", [] (const auto &cfg) { return std::make_unique(cfg); } } - }, - { - "SL1SVG", - { "sl1_svg", [] (const auto &cfg) { return std::make_unique(cfg); } } - }, - { - "SL2", - { "sl1_svg", [] (const auto &cfg) { return std::make_unique(cfg); } } - }, - ANYCUBIC_SLA_FORMAT("pwmo", "Photon Mono"), - ANYCUBIC_SLA_FORMAT("pwmx", "Photon Mono X"), - ANYCUBIC_SLA_FORMAT("pwms", "Photon Mono SE"), -}; - std::unique_ptr SLAArchiveWriter::create(const std::string &archtype, const SLAPrinterConfig &cfg) { - auto entry = REGISTERED_ARCHIVES.find(archtype); + std::unique_ptr ret; + auto factory = get_writer_factory(archtype.c_str()); - if (entry != REGISTERED_ARCHIVES.end()) - return entry->second.factoryfn(cfg); + if (factory) + ret = factory(cfg); - return nullptr; -} - -const std::vector& SLAArchiveWriter::registered_archives() -{ - static std::vector archnames; - - if (archnames.empty()) { - archnames.reserve(REGISTERED_ARCHIVES.size()); - - for (auto &[name, _] : REGISTERED_ARCHIVES) - archnames.emplace_back(name.c_str()); - } - - return archnames; -} - -const char *SLAArchiveWriter::get_extension(const char *archtype) -{ - constexpr const char* DEFAULT_EXT = "zip"; - - auto entry = REGISTERED_ARCHIVES.find(archtype); - if (entry != REGISTERED_ARCHIVES.end()) - return entry->second.ext; - - return DEFAULT_EXT; + return ret; } } // namespace Slic3r diff --git a/src/libslic3r/Format/SLAArchiveWriter.hpp b/src/libslic3r/Format/SLAArchiveWriter.hpp index 86132cceb..1e6ed649b 100644 --- a/src/libslic3r/Format/SLAArchiveWriter.hpp +++ b/src/libslic3r/Format/SLAArchiveWriter.hpp @@ -53,12 +53,6 @@ public: // Factory method to create an archiver instance static std::unique_ptr create( const std::string &archtype, const SLAPrinterConfig &); - - // Get the names of currently known archiver implementations - static const std::vector & registered_archives(); - - // Get the default file extension belonging to an archive format - static const char *get_extension(const char *archtype); }; } // namespace Slic3r diff --git a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp index fed84600c..aa2fb48c1 100644 --- a/src/slic3r/GUI/Jobs/SLAImportDialog.hpp +++ b/src/slic3r/GUI/Jobs/SLAImportDialog.hpp @@ -11,6 +11,7 @@ #include "libslic3r/AppConfig.hpp" #include "libslic3r/Format/SLAArchiveReader.hpp" +#include "libslic3r/Format/SLAArchiveFormatRegistry.hpp" #include "slic3r/GUI/I18N.hpp" @@ -29,11 +30,16 @@ std::string get_readers_wildcard() { std::string ret; - for (const char *archtype : SLAArchiveReader::registered_archives()) { - ret += into_u8(_(SLAArchiveReader::get_description(archtype))); + auto registry = registered_sla_archives(); + + for (const ArchiveEntry &entry : registry) { + if (!entry.rdfactoryfn) + continue; + + ret += into_u8(_(entry.desc)); ret += " ("; - auto extensions = SLAArchiveReader::get_extensions(archtype); - for (const char * ext : extensions) { + std::vector extensions = get_extensions(entry); + for (const std::string &ext : extensions) { ret += "*."; ret += ext; ret += ", "; diff --git a/tests/sla_print/sla_archive_readwrite_tests.cpp b/tests/sla_print/sla_archive_readwrite_tests.cpp index a7ed7f0a4..fb1af3d7f 100644 --- a/tests/sla_print/sla_archive_readwrite_tests.cpp +++ b/tests/sla_print/sla_archive_readwrite_tests.cpp @@ -3,6 +3,7 @@ #include "libslic3r/SLAPrint.hpp" #include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Format/SLAArchiveFormatRegistry.hpp" #include "libslic3r/Format/SLAArchiveWriter.hpp" #include "libslic3r/Format/SLAArchiveReader.hpp" @@ -11,16 +12,18 @@ using namespace Slic3r; TEST_CASE("Archive export test", "[sla_archives]") { + auto registry = registered_sla_archives(); + for (const char * pname : {"20mm_cube", "extruder_idler"}) - for (auto &archname : SLAArchiveWriter::registered_archives()) { - INFO(std::string("Testing archive type: ") + archname + " -- writing..."); + for (const ArchiveEntry &entry : registry) { + INFO(std::string("Testing archive type: ") + entry.id + " -- writing..."); SLAPrint print; SLAFullPrintConfig fullcfg; auto m = Model::read_from_file(TEST_DATA_DIR PATH_SEPARATOR + std::string(pname) + ".obj", nullptr); fullcfg.printer_technology.setInt(ptSLA); // FIXME this should be ensured - fullcfg.set("sla_archive_format", archname); + fullcfg.set("sla_archive_format", entry.id); fullcfg.set("supports_enable", false); fullcfg.set("pad_enable", false); @@ -32,7 +35,7 @@ TEST_CASE("Archive export test", "[sla_archives]") { print.process(); ThumbnailsList thumbnails; - auto outputfname = std::string("output_") + pname + "." + SLAArchiveWriter::get_extension(archname); + auto outputfname = std::string("output_") + pname + "." + entry.ext; print.export_print(outputfname, thumbnails, pname); @@ -41,12 +44,8 @@ TEST_CASE("Archive export test", "[sla_archives]") { double vol_written = m.mesh().volume(); - auto readable_formats = SLAArchiveReader::registered_archives(); - if (std::any_of(readable_formats.begin(), readable_formats.end(), - [&archname](const std::string &a) { return a == archname; })) { - - INFO(std::string("Testing archive type: ") + archname + " -- reading back..."); - + if (entry.rdfactoryfn) { + INFO(std::string("Testing archive type: ") + entry.id + " -- reading back..."); indexed_triangle_set its; DynamicPrintConfig cfg; From a4cf34a49f98866d812a2a237740a7bf96488faa Mon Sep 17 00:00:00 2001 From: YuSanka Date: Mon, 24 Apr 2023 14:58:56 +0200 Subject: [PATCH 115/115] Fix for SPE-1657 : [LINUX - GTK3] Crash when change position of part in sidebar --- src/slic3r/GUI/GUI_ObjectList.cpp | 34 +++++++++++++++++--------- src/slic3r/GUI/ObjectDataViewModel.cpp | 10 ++++++++ src/slic3r/GUI/ObjectDataViewModel.hpp | 1 + 3 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index ffed1eef6..4fa9d6a16 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -1188,6 +1188,13 @@ void ObjectList::key_event(wxKeyEvent& event) void ObjectList::OnBeginDrag(wxDataViewEvent &event) { + if (m_is_editing_started) + m_is_editing_started = false; +#ifdef __WXGTK__ + const auto renderer = dynamic_cast(GetColumn(colName)->GetRenderer()); + renderer->FinishEditing(); +#endif + const wxDataViewItem item(event.GetItem()); const bool mult_sel = multiple_selection(); @@ -1221,18 +1228,11 @@ void ObjectList::OnBeginDrag(wxDataViewEvent &event) m_objects_model->GetInstanceIdByItem(item), type); - /* Under MSW or OSX, DnD moves an item to the place of another selected item - * But under GTK, DnD moves an item between another two items. - * And as a result - call EVT_CHANGE_SELECTION to unselect all items. - * To prevent such behavior use m_prevent_list_events - **/ - m_prevent_list_events = true;//it's needed for GTK - /* Under GTK, DnD requires to the wxTextDataObject been initialized with some valid value, * so set some nonempty string */ wxTextDataObject* obj = new wxTextDataObject; - obj->SetText("Some text");//it's needed for GTK + obj->SetText(mult_sel ? "SomeText" : m_objects_model->GetItemName(item));//it's needed for GTK event.SetDataObject(obj); event.SetDragFlags(wxDrag_DefaultMove); // allows both copy and move; @@ -1295,11 +1295,8 @@ bool ObjectList::can_drop(const wxDataViewItem& item) const void ObjectList::OnDropPossible(wxDataViewEvent &event) { const wxDataViewItem& item = event.GetItem(); - - if (!can_drop(item)) { + if (!can_drop(item)) event.Veto(); - m_prevent_list_events = false; - } } void ObjectList::OnDrop(wxDataViewEvent &event) @@ -1313,6 +1310,13 @@ void ObjectList::OnDrop(wxDataViewEvent &event) return; } + /* Under MSW or OSX, DnD moves an item to the place of another selected item + * But under GTK, DnD moves an item between another two items. + * And as a result - call EVT_CHANGE_SELECTION to unselect all items. + * To prevent such behavior use m_prevent_list_events + **/ + m_prevent_list_events = true;//it's needed for GTK + if (m_dragged_data.type() == itInstance) { Plater::TakeSnapshot snapshot(wxGetApp().plater(),_(L("Instances to Separated Objects"))); @@ -4815,6 +4819,9 @@ void ObjectList::sys_color_changed() void ObjectList::ItemValueChanged(wxDataViewEvent &event) { + if (!m_is_editing_started) + return; + if (event.GetColumn() == colName) update_name_in_model(event.GetItem()); else if (event.GetColumn() == colExtruder) { @@ -4837,6 +4844,9 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event) void ObjectList::OnEditingDone(wxDataViewEvent &event) { + if (!m_is_editing_started) + return; + m_is_editing_started = false; if (event.GetColumn() != colName) return; diff --git a/src/slic3r/GUI/ObjectDataViewModel.cpp b/src/slic3r/GUI/ObjectDataViewModel.cpp index fe57d7d5a..38747d08d 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.cpp +++ b/src/slic3r/GUI/ObjectDataViewModel.cpp @@ -1042,6 +1042,16 @@ int ObjectDataViewModel::GetItemIdByLayerRange(const int obj_idx, const t_layer return GetLayerIdByItem(item); } +wxString ObjectDataViewModel::GetItemName(const wxDataViewItem &item) const +{ + if (!item.IsOk()) + return wxEmptyString; + ObjectDataViewModelNode* node = static_cast(item.GetID()); + if (!node) + return wxEmptyString; + return node->GetName(); +} + int ObjectDataViewModel::GetIdByItem(const wxDataViewItem& item) const { if(!item.IsOk()) diff --git a/src/slic3r/GUI/ObjectDataViewModel.hpp b/src/slic3r/GUI/ObjectDataViewModel.hpp index 993b67842..bc5b485a3 100644 --- a/src/slic3r/GUI/ObjectDataViewModel.hpp +++ b/src/slic3r/GUI/ObjectDataViewModel.hpp @@ -311,6 +311,7 @@ public: wxDataViewItem GetItemByLayerId(int obj_idx, int layer_idx); wxDataViewItem GetItemByLayerRange(const int obj_idx, const t_layer_height_range& layer_range); int GetItemIdByLayerRange(const int obj_idx, const t_layer_height_range& layer_range); + wxString GetItemName(const wxDataViewItem& item) const; int GetIdByItem(const wxDataViewItem& item) const; int GetIdByItemAndType(const wxDataViewItem& item, const ItemType type) const; int GetObjectIdByItem(const wxDataViewItem& item) const;