From 2253ab304aae93c5bbfec2ec1a2bf1e9c1cd83f4 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Wed, 22 Jan 2025 09:58:34 +0800 Subject: [PATCH] Fix arachne wall ordering (#7959) * Revert "SPE-1950: Optimization of computation complexity of perimeter ordering for Arachne generator." This reverts commit 47ec9b9b0692d094f534ee891f30e00198be1338. * Revert "SPE-1963: Improve ordering of perimeters with Arachne perimeter generator" This reverts commit babb84c70ac855a037b9f6b20cacf0190a053aed. --- src/libslic3r/Arachne/PerimeterOrder.cpp | 280 ---- src/libslic3r/Arachne/PerimeterOrder.hpp | 51 - src/libslic3r/Arachne/WallToolPaths.cpp | 94 ++ src/libslic3r/Arachne/WallToolPaths.hpp | 10 + src/libslic3r/Arachne/utils/ExtrusionLine.cpp | 23 +- src/libslic3r/Arachne/utils/ExtrusionLine.hpp | 24 +- src/libslic3r/CMakeLists.txt | 2 - src/libslic3r/PerimeterGenerator.cpp | 1173 +++++++++-------- 8 files changed, 756 insertions(+), 901 deletions(-) delete mode 100644 src/libslic3r/Arachne/PerimeterOrder.cpp delete mode 100644 src/libslic3r/Arachne/PerimeterOrder.hpp diff --git a/src/libslic3r/Arachne/PerimeterOrder.cpp b/src/libslic3r/Arachne/PerimeterOrder.cpp deleted file mode 100644 index c6c2755a4..000000000 --- a/src/libslic3r/Arachne/PerimeterOrder.cpp +++ /dev/null @@ -1,280 +0,0 @@ -#include -#include -#include - -#include "PerimeterOrder.hpp" -#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp" -#include "libslic3r/Point.hpp" - -namespace Slic3r::Arachne::PerimeterOrder { - -using namespace Arachne; - -static size_t get_extrusion_lines_count(const Perimeters &perimeters) { - size_t extrusion_lines_count = 0; - for (const Perimeter &perimeter : perimeters) - extrusion_lines_count += perimeter.size(); - - return extrusion_lines_count; -} - -static PerimeterExtrusions get_sorted_perimeter_extrusions_by_area(const Perimeters &perimeters) { - PerimeterExtrusions sorted_perimeter_extrusions; - sorted_perimeter_extrusions.reserve(get_extrusion_lines_count(perimeters)); - - for (const Perimeter &perimeter : perimeters) { - for (const ExtrusionLine &extrusion_line : perimeter) { - if (extrusion_line.empty()) - continue; // This shouldn't ever happen. - - const BoundingBox bbox = get_extents(extrusion_line); - // Be aware that Arachne produces contours with clockwise orientation and holes with counterclockwise orientation. - const double area = std::abs(extrusion_line.area()); - const Polygon polygon = extrusion_line.is_closed ? to_polygon(extrusion_line) : Polygon{}; - - sorted_perimeter_extrusions.emplace_back(extrusion_line, area, polygon, bbox); - } - } - - // Open extrusions have an area equal to zero, so sorting based on the area ensures that open extrusions will always be before closed ones. - std::sort(sorted_perimeter_extrusions.begin(), sorted_perimeter_extrusions.end(), - [](const PerimeterExtrusion &l, const PerimeterExtrusion &r) { return l.area < r.area; }); - - return sorted_perimeter_extrusions; -} - -// Functions fill adjacent_perimeter_extrusions field for every PerimeterExtrusion by pointers to PerimeterExtrusions that contain or are inside this PerimeterExtrusion. -static void construct_perimeter_extrusions_adjacency_graph(PerimeterExtrusions &sorted_perimeter_extrusions) { - // Construct a graph (defined using adjacent_perimeter_extrusions field) where two PerimeterExtrusion are adjacent when one is inside the other. - std::vector root_candidates(sorted_perimeter_extrusions.size(), false); - for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { - const size_t perimeter_extrusion_idx = &perimeter_extrusion - sorted_perimeter_extrusions.data(); - - if (!perimeter_extrusion.is_closed()) { - root_candidates[perimeter_extrusion_idx] = true; - continue; - } - - for (PerimeterExtrusion &root_candidate : sorted_perimeter_extrusions) { - const size_t root_candidate_idx = &root_candidate - sorted_perimeter_extrusions.data(); - - if (!root_candidates[root_candidate_idx]) - continue; - - if (perimeter_extrusion.bbox.contains(root_candidate.bbox) && perimeter_extrusion.polygon.contains(root_candidate.extrusion.junctions.front().p)) { - perimeter_extrusion.adjacent_perimeter_extrusions.emplace_back(&root_candidate); - root_candidate.adjacent_perimeter_extrusions.emplace_back(&perimeter_extrusion); - root_candidates[root_candidate_idx] = false; - } - } - - root_candidates[perimeter_extrusion_idx] = true; - } -} - -// Perform the depth-first search to assign the nearest external perimeter for every PerimeterExtrusion. -// When some PerimeterExtrusion is achievable from more than one external perimeter, then we choose the -// one that comes from a contour. -static void assign_nearest_external_perimeter(PerimeterExtrusions &sorted_perimeter_extrusions) { - std::stack stack; - for (PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { - if (perimeter_extrusion.is_external_perimeter()) { - perimeter_extrusion.depth = 0; - perimeter_extrusion.nearest_external_perimeter = &perimeter_extrusion; - stack.push(&perimeter_extrusion); - } - } - - while (!stack.empty()) { - PerimeterExtrusion *current_extrusion = stack.top(); - stack.pop(); - - for (PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) { - const size_t adjacent_extrusion_depth = current_extrusion->depth + 1; - // Update depth when the new depth is smaller or when we can achieve the same depth from a contour. - // This will ensure that the internal perimeter will be extruded before the outer external perimeter - // when there are two external perimeters and one internal. - if (adjacent_extrusion_depth < adjacent_extrusion->depth) { - adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter; - adjacent_extrusion->depth = adjacent_extrusion_depth; - stack.push(adjacent_extrusion); - } else if (adjacent_extrusion_depth == adjacent_extrusion->depth && !adjacent_extrusion->nearest_external_perimeter->is_contour() && current_extrusion->is_contour()) { - adjacent_extrusion->nearest_external_perimeter = current_extrusion->nearest_external_perimeter; - stack.push(adjacent_extrusion); - } - } - } -} - -inline Point get_end_position(const ExtrusionLine &extrusion) { - if (extrusion.is_closed) - return extrusion.junctions[0].p; // We ended where we started. - else - return extrusion.junctions.back().p; // Pick the other end from where we started. -} - -// Returns ordered extrusions. -static std::vector ordered_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector extrusions) { - // Ensure that open extrusions will be placed before the closed one. - std::sort(extrusions.begin(), extrusions.end(), - [](const PerimeterExtrusion *l, const PerimeterExtrusion *r) -> bool { return l->is_closed() < r->is_closed(); }); - - std::vector ordered_extrusions; - std::vector already_selected(extrusions.size(), false); - while (ordered_extrusions.size() < extrusions.size()) { - double nearest_distance_sqr = std::numeric_limits::max(); - size_t nearest_extrusion_idx = 0; - bool is_nearest_closed = false; - - for (size_t extrusion_idx = 0; extrusion_idx < extrusions.size(); ++extrusion_idx) { - if (already_selected[extrusion_idx]) - continue; - - const ExtrusionLine &extrusion_line = extrusions[extrusion_idx]->extrusion; - const Point &extrusion_start_position = extrusion_line.junctions.front().p; - const double distance_sqr = (current_position - extrusion_start_position).cast().squaredNorm(); - if (distance_sqr < nearest_distance_sqr) { - if (extrusion_line.is_closed || (!extrusion_line.is_closed && nearest_distance_sqr == std::numeric_limits::max()) || (!extrusion_line.is_closed && !is_nearest_closed)) { - nearest_extrusion_idx = extrusion_idx; - nearest_distance_sqr = distance_sqr; - is_nearest_closed = extrusion_line.is_closed; - } - } - } - - already_selected[nearest_extrusion_idx] = true; - const PerimeterExtrusion *nearest_extrusion = extrusions[nearest_extrusion_idx]; - current_position = get_end_position(nearest_extrusion->extrusion); - ordered_extrusions.emplace_back(nearest_extrusion); - } - - return ordered_extrusions; -} - -struct GroupedPerimeterExtrusions -{ - GroupedPerimeterExtrusions() = delete; - explicit GroupedPerimeterExtrusions(const PerimeterExtrusion *external_perimeter_extrusion) - : external_perimeter_extrusion(external_perimeter_extrusion) {} - - std::vector extrusions; - const PerimeterExtrusion *external_perimeter_extrusion = nullptr; -}; - -// Returns vector of indexes that represent the order of grouped extrusions in grouped_extrusions. -static std::vector order_of_grouped_perimeter_extrusions_to_minimize_distances(Point current_position, std::vector grouped_extrusions) { - // Ensure that holes will be placed before contour and open extrusions before the closed one. - std::sort(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &l, const GroupedPerimeterExtrusions &r) -> bool { - return (l.external_perimeter_extrusion->is_contour() < r.external_perimeter_extrusion->is_contour()) || - (l.external_perimeter_extrusion->is_contour() == r.external_perimeter_extrusion->is_contour() && l.external_perimeter_extrusion->is_closed() < r.external_perimeter_extrusion->is_closed()); - }); - - const size_t holes_cnt = std::count_if(grouped_extrusions.begin(), grouped_extrusions.end(), [](const GroupedPerimeterExtrusions &grouped_extrusions) { - return !grouped_extrusions.external_perimeter_extrusion->is_contour(); - }); - - std::vector grouped_extrusions_order; - std::vector already_selected(grouped_extrusions.size(), false); - while (grouped_extrusions_order.size() < grouped_extrusions.size()) { - double nearest_distance_sqr = std::numeric_limits::max(); - size_t nearest_grouped_extrusions_idx = 0; - bool is_nearest_closed = false; - - // First we order all holes and then we start ordering contours. - const size_t grouped_extrusion_end = grouped_extrusions_order.size() < holes_cnt ? holes_cnt: grouped_extrusions.size(); - for (size_t grouped_extrusion_idx = 0; grouped_extrusion_idx < grouped_extrusion_end; ++grouped_extrusion_idx) { - if (already_selected[grouped_extrusion_idx]) - continue; - - const ExtrusionLine &external_perimeter_extrusion_line = grouped_extrusions[grouped_extrusion_idx].external_perimeter_extrusion->extrusion; - const Point &extrusion_start_position = external_perimeter_extrusion_line.junctions.front().p; - const double distance_sqr = (current_position - extrusion_start_position).cast().squaredNorm(); - if (distance_sqr < nearest_distance_sqr) { - if (external_perimeter_extrusion_line.is_closed || (!external_perimeter_extrusion_line.is_closed && nearest_distance_sqr == std::numeric_limits::max()) || (!external_perimeter_extrusion_line.is_closed && !is_nearest_closed)) { - nearest_grouped_extrusions_idx = grouped_extrusion_idx; - nearest_distance_sqr = distance_sqr; - is_nearest_closed = external_perimeter_extrusion_line.is_closed; - } - } - } - - grouped_extrusions_order.emplace_back(nearest_grouped_extrusions_idx); - already_selected[nearest_grouped_extrusions_idx] = true; - const GroupedPerimeterExtrusions &nearest_grouped_extrusions = grouped_extrusions[nearest_grouped_extrusions_idx]; - const ExtrusionLine &last_extrusion_line = nearest_grouped_extrusions.extrusions.back()->extrusion; - current_position = get_end_position(last_extrusion_line); - } - - return grouped_extrusions_order; -} - -static PerimeterExtrusions extract_ordered_perimeter_extrusions(const PerimeterExtrusions &sorted_perimeter_extrusions, const bool external_perimeters_first) { - // Extrusions are ordered inside each group. - std::vector grouped_extrusions; - - std::stack stack; - std::vector visited(sorted_perimeter_extrusions.size(), false); - for (const PerimeterExtrusion &perimeter_extrusion : sorted_perimeter_extrusions) { - if (!perimeter_extrusion.is_external_perimeter()) - continue; - - stack.push(&perimeter_extrusion); - visited.assign(sorted_perimeter_extrusions.size(), false); - - grouped_extrusions.emplace_back(&perimeter_extrusion); - while (!stack.empty()) { - const PerimeterExtrusion *current_extrusion = stack.top(); - const size_t current_extrusion_idx = current_extrusion - sorted_perimeter_extrusions.data(); - - stack.pop(); - visited[current_extrusion_idx] = true; - - if (current_extrusion->nearest_external_perimeter == &perimeter_extrusion) { - grouped_extrusions.back().extrusions.emplace_back(current_extrusion); - } - - std::vector available_candidates; - for (const PerimeterExtrusion *adjacent_extrusion : current_extrusion->adjacent_perimeter_extrusions) { - const size_t adjacent_extrusion_idx = adjacent_extrusion - sorted_perimeter_extrusions.data(); - if (!visited[adjacent_extrusion_idx] && !adjacent_extrusion->is_external_perimeter() && adjacent_extrusion->nearest_external_perimeter == &perimeter_extrusion) { - available_candidates.emplace_back(adjacent_extrusion); - } - } - - if (available_candidates.size() == 1) { - stack.push(available_candidates.front()); - } else if (available_candidates.size() > 1) { - // When there is more than one available candidate, then order candidates to minimize distances between - // candidates and also to minimize the distance from the current_position. - std::vector adjacent_extrusions = ordered_perimeter_extrusions_to_minimize_distances(Point::Zero(), available_candidates); - for (auto extrusion_it = adjacent_extrusions.rbegin(); extrusion_it != adjacent_extrusions.rend(); ++extrusion_it) { - stack.push(*extrusion_it); - } - } - } - - if (!external_perimeters_first) - std::reverse(grouped_extrusions.back().extrusions.begin(), grouped_extrusions.back().extrusions.end()); - } - - const std::vector grouped_extrusion_order = order_of_grouped_perimeter_extrusions_to_minimize_distances(Point::Zero(), grouped_extrusions); - - PerimeterExtrusions ordered_extrusions; - for (size_t order_idx : grouped_extrusion_order) { - for (const PerimeterExtrusion *perimeter_extrusion : grouped_extrusions[order_idx].extrusions) - ordered_extrusions.emplace_back(*perimeter_extrusion); - } - - return ordered_extrusions; -} - -// FIXME: From the point of better patch planning, it should be better to do ordering when we have generated all extrusions (for now, when G-Code is exported). -// FIXME: It would be better to extract the adjacency graph of extrusions from the SkeletalTrapezoidation graph. -PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, const bool external_perimeters_first) { - PerimeterExtrusions sorted_perimeter_extrusions = get_sorted_perimeter_extrusions_by_area(perimeters); - construct_perimeter_extrusions_adjacency_graph(sorted_perimeter_extrusions); - assign_nearest_external_perimeter(sorted_perimeter_extrusions); - return extract_ordered_perimeter_extrusions(sorted_perimeter_extrusions, external_perimeters_first); -} - -} // namespace Slic3r::Arachne::PerimeterOrder \ No newline at end of file diff --git a/src/libslic3r/Arachne/PerimeterOrder.hpp b/src/libslic3r/Arachne/PerimeterOrder.hpp deleted file mode 100644 index f8469d917..000000000 --- a/src/libslic3r/Arachne/PerimeterOrder.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#ifndef slic3r_GCode_PerimeterOrder_hpp_ -#define slic3r_GCode_PerimeterOrder_hpp_ - -#include -#include -#include -#include - -#include "libslic3r/Arachne/utils/ExtrusionLine.hpp" -#include "libslic3r/BoundingBox.hpp" -#include "libslic3r/Polygon.hpp" - -namespace Slic3r::Arachne::PerimeterOrder { - -// Data structure stores ExtrusionLine (closed and open) together with additional data. -struct PerimeterExtrusion -{ - explicit PerimeterExtrusion(const Arachne::ExtrusionLine &extrusion, const double area, const Polygon &polygon, const BoundingBox &bbox) - : extrusion(extrusion), area(area), polygon(polygon), bbox(bbox) {} - - Arachne::ExtrusionLine extrusion; - // Absolute value of the area of the polygon. The value is always non-negative, even for holes. - double area = 0; - - // Polygon is non-empty only for closed extrusions. - Polygon polygon; - BoundingBox bbox; - - std::vector adjacent_perimeter_extrusions; - - // How far is this perimeter from the nearest external perimeter. Contour is always preferred over holes. - size_t depth = std::numeric_limits::max(); - PerimeterExtrusion *nearest_external_perimeter = nullptr; - - // Returns if ExtrusionLine is a contour or a hole. - bool is_contour() const { return extrusion.is_contour(); } - - // Returns if ExtrusionLine is closed or opened. - bool is_closed() const { return extrusion.is_closed; } - - // Returns if ExtrusionLine is an external or an internal perimeter. - bool is_external_perimeter() const { return extrusion.is_external_perimeter(); } -}; - -using PerimeterExtrusions = std::vector; - -PerimeterExtrusions ordered_perimeter_extrusions(const Perimeters &perimeters, bool external_perimeters_first); - -} // namespace Slic3r::Arachne::PerimeterOrder - -#endif // slic3r_GCode_Travels_hpp_ diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index 8f2b04cf4..b5b354837 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -782,4 +782,98 @@ bool WallToolPaths::removeEmptyToolPaths(std::vector &toolpa return toolpaths.empty(); } +/*! + * 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. + * + * Odd walls should always go after their enclosing wall polygons. + * + * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. + */ +WallToolPaths::ExtrusionLineSet WallToolPaths::getRegionOrder(const std::vector &input, const bool outer_to_inner) +{ + 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, + // which is the requirement for there to be an order constraint. + // + // We use a PointGrid rather than a LineGrid to save on computation time. + // In very rare cases two insets might lie next to each other without having neighboring vertices, e.g. + // \ . + // | / . + // | / . + // || . + // | \ . + // | \ . + // / . + // However, because of how Arachne works this will likely never be the case for two consecutive insets. + // On the other hand one could imagine that two consecutive insets of a very large circle + // could be simplify()ed such that the remaining vertices of the two insets don't align. + // In those cases the order requirement is not captured, + // which means that the PathOrderOptimizer *might* result in a violation of the user set path order. + // This problem is expected to be not so severe and happen very sparsely. + + coord_t max_line_w = 0u; + for (const ExtrusionLine *line : input) // compute max_line_w + for (const ExtrusionJunction &junction : *line) + max_line_w = std::max(max_line_w, junction.w); + if (max_line_w == 0u) + return order_requirements; + + struct LineLoc + { + ExtrusionJunction j; + const ExtrusionLine *line; + }; + struct Locator + { + Point operator()(const LineLoc &elem) { return elem.j.p; } + }; + + // How much farther two verts may be apart due to corners. + // This distance must be smaller than 2, because otherwise + // we could create an order requirement between e.g. + // wall 2 of one region and wall 3 of another region, + // while another wall 3 of the first region would lie in between those two walls. + // However, higher values are better against the limitations of using a PointGrid rather than a LineGrid. + constexpr float diagonal_extension = 1.9f; + const auto searching_radius = coord_t(max_line_w * diagonal_extension); + using GridT = SparsePointGrid; + GridT grid(searching_radius); + + for (const ExtrusionLine *line : input) + for (const ExtrusionJunction &junction : *line) grid.insert(LineLoc{junction, line}); + for (const std::pair &pair : grid) { + const LineLoc &lineloc_here = pair.second; + const ExtrusionLine *here = lineloc_here.line; + Point loc_here = pair.second.j.p; + std::vector nearby_verts = grid.getNearby(loc_here, searching_radius); + for (const LineLoc &lineloc_nearby : nearby_verts) { + const ExtrusionLine *nearby = lineloc_nearby.line; + if (nearby == here) + continue; + if (nearby->inset_idx == here->inset_idx) + continue; + if (nearby->inset_idx > here->inset_idx + 1) + continue; // not directly adjacent + if (here->inset_idx > nearby->inset_idx + 1) + continue; // not directly adjacent + if (!shorter_then(loc_here - lineloc_nearby.j.p, (lineloc_here.j.w + lineloc_nearby.j.w) / 2 * diagonal_extension)) + continue; // points are too far away from each other + if (here->is_odd || nearby->is_odd) { + if (here->is_odd && !nearby->is_odd && nearby->inset_idx < here->inset_idx) + order_requirements.emplace(std::make_pair(nearby, here)); + if (nearby->is_odd && !here->is_odd && here->inset_idx < nearby->inset_idx) + order_requirements.emplace(std::make_pair(here, nearby)); + } else if ((nearby->inset_idx < here->inset_idx) == outer_to_inner) { + order_requirements.emplace(std::make_pair(nearby, here)); + } else { + assert((nearby->inset_idx > here->inset_idx) == outer_to_inner); + order_requirements.emplace(std::make_pair(here, nearby)); + } + } + } + return order_requirements; +} + } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp index 00652fc09..457f7e714 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -90,6 +90,16 @@ public: 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. + * + * Odd walls should always go after their enclosing wall polygons. + * + * \param outer_to_inner Whether the wall polygons with a lower inset_idx should go before those with a higher one. + */ + static ExtrusionLineSet getRegionOrder(const std::vector &input, bool outer_to_inner); + protected: /*! * Stitch the polylines together and form closed polygons. diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp index 49449cc59..0ee40b2b4 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp @@ -264,10 +264,9 @@ bool ExtrusionLine::is_contour() const return poly.is_clockwise(); } -double ExtrusionLine::area() const { - if (!this->is_closed) - return 0.; - +double ExtrusionLine::area() const +{ + assert(this->is_closed); double a = 0.; if (this->junctions.size() >= 3) { Vec2d p1 = this->junctions.back().p.cast(); @@ -277,25 +276,9 @@ double ExtrusionLine::area() const { p1 = p2; } } - return 0.5 * a; } -Points to_points(const ExtrusionLine &extrusion_line) { - Points points; - points.reserve(extrusion_line.junctions.size()); - for (const ExtrusionJunction &junction : extrusion_line.junctions) - points.emplace_back(junction.p); - return points; -} - -BoundingBox get_extents(const ExtrusionLine &extrusion_line) { - BoundingBox bbox; - for (const ExtrusionJunction &junction : extrusion_line.junctions) - bbox.merge(junction.p); - return bbox; -} - } // namespace Slic3r::Arachne namespace Slic3r { diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp index d8cad702a..21791000f 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -199,8 +199,6 @@ struct ExtrusionLine bool is_contour() const; double area() const; - - bool is_external_perimeter() const { return this->inset_idx == 0; } }; template @@ -227,7 +225,6 @@ static inline Slic3r::ThickPolyline to_thick_polyline(const PathType &path) static inline Polygon to_polygon(const ExtrusionLine &line) { Polygon out; - assert(line.is_closed); assert(line.junctions.size() >= 3); assert(line.junctions.front().p == line.junctions.back().p); out.points.reserve(line.junctions.size() - 1); @@ -236,11 +233,24 @@ static inline Polygon to_polygon(const ExtrusionLine &line) return out; } -Points to_points(const ExtrusionLine &extrusion_line); - -BoundingBox get_extents(const ExtrusionLine &extrusion_line); +static Points to_points(const ExtrusionLine &extrusion_line) +{ + Points points; + points.reserve(extrusion_line.junctions.size()); + for (const ExtrusionJunction &junction : extrusion_line.junctions) + points.emplace_back(junction.p); + return points; +} #if 0 +static BoundingBox get_extents(const ExtrusionLine &extrusion_line) +{ + BoundingBox bbox; + for (const ExtrusionJunction &junction : extrusion_line.junctions) + bbox.merge(junction.p); + return bbox; +} + static BoundingBox get_extents(const std::vector &extrusion_lines) { BoundingBox bbox; @@ -271,8 +281,6 @@ static std::vector to_points(const std::vector &e #endif using VariableWidthLines = std::vector; //; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 9dae196b3..ea08f39cf 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -433,8 +433,6 @@ set(lisbslic3r_sources Arachne/utils/PolygonsSegmentIndex.hpp Arachne/utils/PolylineStitcher.hpp Arachne/utils/PolylineStitcher.cpp - Arachne/PerimeterOrder.hpp - Arachne/PerimeterOrder.cpp Arachne/SkeletalTrapezoidation.hpp Arachne/SkeletalTrapezoidation.cpp Arachne/SkeletalTrapezoidationEdge.hpp diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 5c142db47..624a71090 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -9,7 +9,6 @@ #include "VariableWidth.hpp" #include "CurveAnalyzer.hpp" #include "Clipper2Utils.hpp" -#include "Arachne/PerimeterOrder.hpp" #include "Arachne/WallToolPaths.hpp" #include "Geometry/ConvexHull.hpp" #include "ExPolygonCollection.hpp" @@ -567,7 +566,7 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime for (const auto & r : fuzzified_regions) { BoundingBox bbox = get_extents(perimeter_generator.slices->surfaces); bbox.offset(scale_(1.)); - ::Slic3r::SVG svg(debug_out_path("fuzzy_traverse_loops_%d_%d_%d_region_%d.svg", perimeter_generator.layer_id, loop.is_contour() ? 0 : 1, loop.depth, i).c_str(), bbox); + ::Slic3r::SVG svg(debug_out_path("fuzzy_traverse_loops_%d_%d_%d_region_%d.svg", perimeter_generator.layer_id, loop.is_contour ? 0 : 1, loop.depth, i).c_str(), bbox); svg.draw_outline(perimeter_generator.slices->surfaces); svg.draw_outline(loop.polygon, "green"); svg.draw(r.second, "red", 0.5); @@ -877,6 +876,13 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con return clipped_paths; } +struct PerimeterGeneratorArachneExtrusion +{ + Arachne::ExtrusionLine* extrusion = nullptr; + // Indicates if closed ExtrusionLine is a contour or a hole. Used it only when ExtrusionLine is a closed loop. + bool is_contour = false; +}; + static void smooth_overhang_level(ExtrusionPaths &paths) { @@ -961,7 +967,7 @@ static void smooth_overhang_level(ExtrusionPaths &paths) } } -static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& perimeter_generator, Arachne::PerimeterOrder::PerimeterExtrusions& pg_extrusions, +static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& perimeter_generator, std::vector& pg_extrusions, bool &steep_overhang_contour, bool &steep_overhang_hole) { // Detect steep overhangs @@ -969,34 +975,34 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p perimeter_generator.layer_id % 2 == 1; // Only calculate overhang degree on even (from GUI POV) layers ExtrusionEntityCollection extrusion_coll; - for (Arachne::PerimeterOrder::PerimeterExtrusion &pg_extrusion : pg_extrusions) { - Arachne::ExtrusionLine &extrusion = pg_extrusion.extrusion; - if (extrusion.empty()) + for (PerimeterGeneratorArachneExtrusion& pg_extrusion : pg_extrusions) { + Arachne::ExtrusionLine* extrusion = pg_extrusion.extrusion; + if (extrusion->empty()) continue; - const bool is_external = extrusion.inset_idx == 0; + const bool is_external = extrusion->inset_idx == 0; ExtrusionRole role = is_external ? erExternalPerimeter : erPerimeter; const auto& regions = perimeter_generator.regions_by_fuzzify; - const bool is_contour = !extrusion.is_closed || pg_extrusion.is_contour(); + const bool is_contour = !extrusion->is_closed || pg_extrusion.is_contour; if (regions.size() == 1) { // optimization const auto& config = regions.begin()->first; - const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion.inset_idx, is_contour); + const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); if (fuzzify) - fuzzy_extrusion_line(extrusion.junctions, config); + fuzzy_extrusion_line(extrusion->junctions, config); } else { // Find all affective regions std::vector> fuzzified_regions; fuzzified_regions.reserve(regions.size()); for (const auto& region : regions) { - if (should_fuzzify(region.first, perimeter_generator.layer_id, extrusion.inset_idx, is_contour)) { + if (should_fuzzify(region.first, perimeter_generator.layer_id, extrusion->inset_idx, is_contour)) { fuzzified_regions.emplace_back(region.first, region.second); } } if (!fuzzified_regions.empty()) { // Split the loops into lines with different config, and fuzzy them separately for (const auto& r : fuzzified_regions) { - const auto splitted = Algorithm::split_line(extrusion, r.second, false); + const auto splitted = Algorithm::split_line(*extrusion, r.second, false); if (splitted.empty()) { // No intersection, skip continue; @@ -1005,19 +1011,19 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // Fuzzy splitted extrusion if (std::all_of(splitted.begin(), splitted.end(), [](const Algorithm::SplitLineJunction& j) { return j.clipped; })) { // The entire polygon is fuzzified - fuzzy_extrusion_line(extrusion.junctions, r.first); + fuzzy_extrusion_line(extrusion->junctions, r.first); } else { - const auto current_ext = extrusion.junctions; + const auto current_ext = extrusion->junctions; std::vector segment; segment.reserve(current_ext.size()); - extrusion.junctions.clear(); + extrusion->junctions.clear(); - const auto fuzzy_current_segment = [&segment, &extrusion, &r]() { - extrusion.junctions.push_back(segment.front()); + const auto fuzzy_current_segment = [&segment, extrusion, &r]() { + extrusion->junctions.push_back(segment.front()); const auto back = segment.back(); fuzzy_extrusion_line(segment, r.first); - extrusion.junctions.insert(extrusion.junctions.end(), segment.begin(), segment.end()); - extrusion.junctions.push_back(back); + extrusion->junctions.insert(extrusion->junctions.end(), segment.begin(), segment.end()); + extrusion->junctions.push_back(back); segment.clear(); }; @@ -1034,7 +1040,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p segment.push_back(to_ex_junction(p)); } else { if (segment.empty()) { - extrusion.junctions.push_back(to_ex_junction(p)); + extrusion->junctions.push_back(to_ex_junction(p)); } else { segment.push_back(to_ex_junction(p)); fuzzy_current_segment(); @@ -1053,9 +1059,9 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // detect overhanging/bridging perimeters if (perimeter_generator.config->detect_overhang_wall && perimeter_generator.layer_id > perimeter_generator.object_config->raft_layers) { ClipperLib_Z::Path extrusion_path; - extrusion_path.reserve(extrusion.size()); + extrusion_path.reserve(extrusion->size()); BoundingBox extrusion_path_bbox; - for (const Arachne::ExtrusionJunction &ej : extrusion.junctions) { + for (const Arachne::ExtrusionJunction &ej : extrusion->junctions) { extrusion_path.emplace_back(ej.p.x(), ej.p.y(), ej.w); extrusion_path_bbox.merge(Point(ej.p.x(), ej.p.y())); } @@ -1085,7 +1091,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // Always reverse extrusion if use fuzzy skin: https://github.com/SoftFever/OrcaSlicer/pull/2413#issuecomment-1769735357 if (overhangs_reverse && perimeter_generator.has_fuzzy_skin) { - if (pg_extrusion.is_contour()) { + if (pg_extrusion.is_contour) { steep_overhang_contour = true; } else if (perimeter_generator.has_fuzzy_hole) { steep_overhang_hole = true; @@ -1093,7 +1099,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p } // Detect steep overhang // Skip the check if we already found steep overhangs - bool found_steep_overhang = (pg_extrusion.is_contour() && steep_overhang_contour) || (!pg_extrusion.is_contour() && steep_overhang_hole); + bool found_steep_overhang = (pg_extrusion.is_contour && steep_overhang_contour) || (!pg_extrusion.is_contour && steep_overhang_hole); if (overhangs_reverse && !found_steep_overhang) { std::map recognization_paths; for (const ExtrusionPath &path : temp_paths) { @@ -1111,7 +1117,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p BoundingBox extrusion_bboxs = get_extents(be_clipped); - if (detect_steep_overhang(perimeter_generator.config, pg_extrusion.is_contour(), extrusion_bboxs, it.first, be_clipped, perimeter_generator.lower_slices, + if (detect_steep_overhang(perimeter_generator.config, pg_extrusion.is_contour, extrusion_bboxs, it.first, be_clipped, perimeter_generator.lower_slices, steep_overhang_contour, steep_overhang_hole)) { break; } @@ -1179,7 +1185,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p // Arachne sometimes creates extrusion with zero-length (just two same endpoints); if (!paths.empty()) { Point start_point = paths.front().first_point(); - if (!extrusion.is_closed) { + if (!extrusion->is_closed) { // Especially for open extrusion, we need to select a starting point that is at the start // or the end of the extrusions to make one continuous line. Also, we prefer a non-overhang // starting point. @@ -1219,7 +1225,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p if (overhangs_reverse) { for (const ExtrusionPath& path : paths) { if (path.role() == erOverhangPerimeter) { - if (pg_extrusion.is_contour()) + if (pg_extrusion.is_contour) steep_overhang_contour = true; else steep_overhang_hole = true; @@ -1236,13 +1242,13 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p steep_overhang_hole = true; } - extrusion_paths_append(paths, extrusion, role, is_external ? perimeter_generator.ext_perimeter_flow : perimeter_generator.perimeter_flow); + extrusion_paths_append(paths, *extrusion, role, is_external ? perimeter_generator.ext_perimeter_flow : perimeter_generator.perimeter_flow); } // Append paths to collection. if (!paths.empty()) { - if (extrusion.is_closed) { - ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour() ? elrDefault : elrHole); + if (extrusion->is_closed) { + ExtrusionLoop extrusion_loop(std::move(paths), pg_extrusion.is_contour ? elrDefault : elrHole); extrusion_loop.make_counter_clockwise(); // TODO: it seems in practice that ExtrusionLoops occasionally have significantly disconnected paths, // triggering the asserts below. Is this a problem? @@ -1877,516 +1883,6 @@ static void group_region_by_fuzzify(PerimeterGenerator& g) } } - -// ORCA: -// Inner Outer Inner wall ordering mode perimeter order optimisation functions -/** - * @brief Finds all perimeters touching a given set of reference lines, given as indexes. - * - * @param entities The list of PerimeterGeneratorArachneExtrusion entities. - * @param referenceIndices A set of indices representing the reference points. - * @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 - * @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ - * @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc). - * @return std::vector A vector of indices representing the touching perimeters. - */ -std::vector findAllTouchingPerimeters(const Arachne::PerimeterOrder::PerimeterExtrusions& entities, const std::unordered_set& referenceIndices, size_t threshold_external, size_t threshold_internal , size_t considered_inset_idx) { - std::unordered_set touchingIndices; - - for (const int refIdx : referenceIndices) { - const auto& referenceEntity = entities[refIdx]; - Points referencePoints = Arachne::to_points(referenceEntity.extrusion); - for (size_t i = 0; i < entities.size(); ++i) { - // Skip already considered references and the reference entity - if (referenceIndices.count(i) > 0) continue; - const auto& entity = entities[i]; - if (entity.extrusion.is_external_perimeter()) continue; // Ignore inset index 0 (external) perimeters from the re-ordering even if they are touching - - if (entity.extrusion.inset_idx != considered_inset_idx) { // Find Inset index perimeters that match the requested inset index - continue; // skip if they dont match - } - - Points points = Arachne::to_points(entity.extrusion); - double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points); - // Add to touchingIndices if within threshold distance - size_t threshold=0; - if(referenceEntity.extrusion.is_external_perimeter()) - threshold = threshold_external; - else - threshold = threshold_internal; - if (distance <= threshold) { - touchingIndices.insert(i); - } - } - } - return std::vector(touchingIndices.begin(), touchingIndices.end()); -} - -/** - * @brief Reorders perimeters based on proximity to the reference perimeter - * - * This approach finds all perimeters touching the external perimeter first and then finds all perimeters touching these new ones until none are left - * It ensures a level-by-level traversal, similar to BFS in graph theory. - * - * @param entities The list of PerimeterGeneratorArachneExtrusion entities. - * @param referenceIndex The index of the reference perimeter. - * @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 - * @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ - * @return Arachne::PerimeterOrder::PerimeterExtrusions The reordered list of perimeters based on proximity. - */ -Arachne::PerimeterOrder::PerimeterExtrusions reorderPerimetersByProximity(Arachne::PerimeterOrder::PerimeterExtrusions entities, size_t threshold_external, size_t threshold_internal) { - Arachne::PerimeterOrder::PerimeterExtrusions reordered; - std::unordered_set includedIndices; - - // Function to reorder perimeters starting from a given reference index - auto reorderFromReference = [&](int referenceIndex) { - std::unordered_set firstLevelIndices; - firstLevelIndices.insert(referenceIndex); - - // Find first level touching perimeters - std::vector firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1); - // Bring the largest first level perimeter to the front - // The longest first neighbour is most likely the dominant proximate perimeter - // hence printing it immediately after the external perimeter should speed things up - if (!firstLevelTouchingIndices.empty()) { - auto maxIt = std::max_element(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), [&entities](int a, int b) { - return entities[a].extrusion.getLength() < entities[b].extrusion.getLength(); - }); - std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); - } - // Insert first level perimeters into reordered list - reordered.push_back(entities[referenceIndex]); - includedIndices.insert(referenceIndex); - - for (int idx : firstLevelTouchingIndices) { - if (includedIndices.count(idx) == 0) { - reordered.push_back(entities[idx]); - includedIndices.insert(idx); - } - } - - // Loop through all inset indices above 1 - size_t currentInsetIndex = 2; - while (true) { - std::unordered_set currentLevelIndices(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end()); - std::vector currentLevelTouchingIndices = findAllTouchingPerimeters(entities, currentLevelIndices, threshold_external, threshold_internal, currentInsetIndex); - - // Break if no more touching perimeters are found - if (currentLevelTouchingIndices.empty()) { - break; - } - - // Exclude any already included indices from the current level touching indices - currentLevelTouchingIndices.erase( - std::remove_if(currentLevelTouchingIndices.begin(), currentLevelTouchingIndices.end(), - [&](int idx) { return includedIndices.count(idx) > 0; }), - currentLevelTouchingIndices.end()); - - // Bring the largest current level perimeter to the end - if (!currentLevelTouchingIndices.empty()) { - auto maxIt = std::max_element(currentLevelTouchingIndices.begin(), currentLevelTouchingIndices.end(), [&entities](int a, int b) { - return entities[a].extrusion.getLength() < entities[b].extrusion.getLength(); - }); - std::iter_swap(maxIt, currentLevelTouchingIndices.begin()); - } - - // Insert current level perimeters into reordered list - for (int idx : currentLevelTouchingIndices) { - if (includedIndices.count(idx) == 0) { - reordered.push_back(entities[idx]); - includedIndices.insert(idx); - } - } - - // Prepare for the next level - firstLevelTouchingIndices = currentLevelTouchingIndices; - currentInsetIndex++; - } - }; - - // Loop through all perimeters and reorder starting from each inset index 0 perimeter - for (size_t refIdx = 0; refIdx < entities.size(); ++refIdx) { - if (entities[refIdx].extrusion.is_external_perimeter() && includedIndices.count(refIdx) == 0) { - reorderFromReference(refIdx); - } - } - - // Append any remaining entities that were not included - for (size_t i = 0; i < entities.size(); ++i) { - if (includedIndices.count(i) == 0) { - reordered.push_back(entities[i]); - } - } - - return reordered; -} - -/** - * @brief Reorders the vector to bring external perimeter (i.e. paths with inset index 0) that are also contours (i.e. external facing lines) to the front. - * - * This function uses a stable partition to move all external perimeter contour elements to the front of the vector, - * while maintaining the relative order of non-contour elements. - * - * @param ordered_extrusions The vector of PerimeterGeneratorArachneExtrusion to reorder. - */ -void bringContoursToFront(Arachne::PerimeterOrder::PerimeterExtrusions& ordered_extrusions) { - std::stable_partition(ordered_extrusions.begin(), ordered_extrusions.end(), [](const Arachne::PerimeterOrder::PerimeterExtrusion& extrusion) { - return (extrusion.extrusion.is_contour() && extrusion.extrusion.is_external_perimeter()); - }); -} -// ORCA: -// Inner Outer Inner wall ordering mode perimeter order optimisation functions ended - -// Thanks, Cura developers, for implementing an algorithm for generating perimeters with variable width (Arachne) that is based on the paper -// "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" -void PerimeterGenerator::process_arachne() -{ - group_region_by_fuzzify(*this); - - // other perimeters - m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); - coord_t perimeter_spacing = this->perimeter_flow.scaled_spacing(); - - // external perimeters - m_ext_mm3_per_mm = this->ext_perimeter_flow.mm3_per_mm(); - coord_t ext_perimeter_width = this->ext_perimeter_flow.scaled_width(); - coord_t ext_perimeter_spacing = this->ext_perimeter_flow.scaled_spacing(); - coord_t ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.spacing() + this->perimeter_flow.spacing())); - // overhang perimeters - m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); - - // solid infill - coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); - - // prepare grown lower layer slices for overhang detection - if (this->lower_slices != nullptr && this->config->detect_overhang_wall) { - // We consider overhang any part where the entire nozzle diameter is not supported by the - // lower layer, so we take lower slices and offset them by half the nozzle diameter used - // in the current layer - double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); - m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2))); - } - - Surfaces all_surfaces = this->slices->surfaces; - - process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width); - // BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled - double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution; - // we need to process each island separately because we might have different - // extra perimeters for each one - for (const Surface& surface : all_surfaces) { - coord_t bead_width_0 = ext_perimeter_spacing; - // detect how many perimeters must be generated for this island - int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops - int sparse_infill_density = this->config->sparse_infill_density.value; - if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall - loop_number++; - - // Set the bottommost layer to be one wall - const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false; - if (is_bottom_layer && this->config->only_one_wall_first_layer) - loop_number = 0; - - // Orca: set the topmost layer to be one wall according to the config - const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false; - if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top) - loop_number = 0; - - auto apply_precise_outer_wall = config->precise_outer_wall && this->config->wall_sequence == WallSequence::InnerOuter; - // Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled. - ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution), - apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing ) - : -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); - - Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config); - // Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity. - input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false; - - coord_t wall_0_inset = 0; - if (apply_precise_outer_wall) - wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); - - //PS: One wall top surface for Arachne - ExPolygons top_expolygons; - // Calculate how many inner loops remain when TopSurfaces is selected. - const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1; - - // Set one perimeter when TopSurfaces is selected. - if (config->only_one_wall_top) - loop_number = 0; - - Arachne::WallToolPathsParams input_params_tmp = input_params; - - Polygons last_p = to_polygons(last); - Arachne::WallToolPaths wall_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1), - wall_0_inset, layer_height, input_params_tmp); - Arachne::Perimeters perimeters = wall_tool_paths.getToolPaths(); - ExPolygons infill_contour = union_ex(wall_tool_paths.getInnerContour()); - - // Check if there are some remaining perimeters to generate (the number of perimeters - // is greater than one together with enabled the single perimeter on top surface feature). - if (inner_loop_number >= 0) { - assert(upper_slices != nullptr); - - // Infill contour bounding box. - BoundingBox infill_contour_bbox = get_extents(infill_contour); - infill_contour_bbox.offset(SCALED_EPSILON); - - coord_t perimeter_width = this->perimeter_flow.scaled_width(); - - // Get top ExPolygons from current infill contour. - Polygons upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox); - top_expolygons = diff_ex(infill_contour, upper_slices_clipped); - - if (!top_expolygons.empty()) { - if (lower_slices != nullptr) { - const float bridge_offset = float(std::max(ext_perimeter_spacing, perimeter_width)); - const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox); - const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset); - - // Remove bridges from top surface polygons. - top_expolygons = diff_ex(top_expolygons, current_slices_bridges); - } - - // Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line. - // ORCA: skip if the top surface area is smaller than "min_width_top_surface" - const float top_surface_min_width = std::max(float(ext_perimeter_spacing) / 4.f + scaled(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f); - // Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little. - // ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount, - // not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions - // due to thin lines being generated - top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85)); - - // Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges). - const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons); - - // Get final top ExPolygons. - top_expolygons = intersection_ex(top_expolygons, infill_contour); - - const Polygons not_top_polygons = to_polygons(not_top_expolygons); - Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp); - Arachne::Perimeters inner_perimeters = inner_wall_tool_paths.getToolPaths(); - - // Recalculate indexes of inner perimeters before merging them. - if (!perimeters.empty()) { - for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) { - if (inner_perimeter.empty()) - continue; - for (Arachne::ExtrusionLine &el : inner_perimeter) - ++el.inset_idx; - } - } - - perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end()); - infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour()); - } else { - // There is no top surface ExPolygon, so we call Arachne again with parameters - // like when the single perimeter feature is disabled. - Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp); - perimeters = no_single_perimeter_tool_paths.getToolPaths(); - infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour()); - } - } - //PS - - loop_number = int(perimeters.size()) - 1; - -#ifdef ARACHNE_DEBUG - { - static int iRun = 0; - export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", params.layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); - } -#endif - - // All closed ExtrusionLine should have the same the first and the last point. - // But in rare cases, Arachne produce ExtrusionLine marked as closed but without - // equal the first and the last point. - assert([&perimeters = std::as_const(perimeters)]() -> bool { - for (const Arachne::Perimeter& perimeter : perimeters) - for (const Arachne::ExtrusionLine& el : perimeter) - if (el.is_closed && el.junctions.front().p != el.junctions.back().p) - return false; - return true; - }()); - - bool is_outer_wall_first = - this->config->wall_sequence == WallSequence::OuterInner || - this->config->wall_sequence == WallSequence::InnerOuterInner; - - if (layer_id == 0){ // disable inner outer inner algorithm after the first layer - is_outer_wall_first = - this->config->wall_sequence == WallSequence::OuterInner; - } - Arachne::PerimeterOrder::PerimeterExtrusions ordered_extrusions = Arachne::PerimeterOrder::ordered_perimeter_extrusions(perimeters, is_outer_wall_first); - - // printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer - if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer - if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering - int position = 0; // index to run the re-ordering for multiple external perimeters in a single island. - int arr_i, arr_j = 0; // indexes to run through the walls in the for loops - int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values - - // To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering - // for OI mode that is used the basis for IOI - bringContoursToFront(ordered_extrusions); - Arachne::PerimeterOrder::PerimeterExtrusions reordered_extrusions; - - // Debug statement to print spacing values: - //printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing()); - - // Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by 3% to cover - // rounding errors - coord_t threshold_external = (this->ext_perimeter_flow.scaled_spacing()/2 + this->perimeter_flow.scaled_spacing()/2)*1.03; - - // For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by 3% to cover rounding errors. - coord_t threshold_internal = this->perimeter_flow.scaled_spacing() * 1.03; - - // Re-order extrusions based on distance - // Alorithm will aggresively optimise for the appearance of the outermost perimeter - ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal ); - reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list. - - // Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis - // scan to find the external perimeter, first internal, second internal and last perimeter in the island. - // We then advance the position index to move to the second island and continue until there are no more - // perimeters left. - while (position < reordered_extrusions.size()) { - outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1 - max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list - // run through the walls to get the index values that need re-ordering until the first one for each - // is found. Start at "position" index to enable the for loop to iterate for multiple external - // perimeters in a single island - // printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal); - for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { - // printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i); - switch (reordered_extrusions[arr_i].extrusion.inset_idx) { - case 0: // external perimeter - if (outer == -1) - outer = arr_i; - break; - case 1: // first internal wall - if (first_internal==-1 && arr_i>outer && outer!=-1){ - first_internal = arr_i; - } - break; - case 2: // second internal wall - if (second_internal == -1 && arr_i > first_internal && outer!=-1){ - second_internal = arr_i; - } - break; - } - if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion.is_external_perimeter()){ // found a new external perimeter after we've found at least a first internal perimeter to re-order. - // This means we entered a new island. - arr_i=arr_i-1; //step back one perimeter - max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island. - break; // exit the for loop - } - } - - // printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal); - if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in. - Arachne::PerimeterOrder::PerimeterExtrusions inner_outer_extrusions; // temporary array to hold extrusions for reordering - inner_outer_extrusions.resize(max_internal - position + 1, Arachne::PerimeterOrder::PerimeterExtrusion{{},0.0,{},{}}); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations - // printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position); - - for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2 - if(arr_j >= second_internal){ - //printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j); - inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j]; - current_perimeter++; - } - } - - for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order - // printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1); - inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j]; - } - - for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array - ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position]; - } - // go to the next perimeter from the current position to continue scanning for external walls in the same island - position = arr_i + 1; - } - } - } - - bool steep_overhang_contour = false; - bool steep_overhang_hole = false; - const WallDirection wall_direction = config->wall_direction; - if (wall_direction != WallDirection::Auto) { - // Skip steep overhang detection if wall direction is specified - steep_overhang_contour = true; - steep_overhang_hole = true; - } - if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { - // All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want - if (wall_direction != WallDirection::CounterClockwise) { - reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, - // Reverse internal only if the wall direction is auto - this->config->overhang_reverse_internal_only && wall_direction == WallDirection::Auto); - } - this->loops->append(extrusion_coll); - } - - const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing; - - if (offset_ex(infill_contour, -float(spacing / 2.)).empty()) - infill_contour.clear(); // Infill region is too small, so let's filter it out. - - // create one more offset to be used as boundary for fill - // we offset by half the perimeter spacing (to get to the actual infill boundary) - // and then we offset back and forth by half the infill spacing to only consider the - // non-collapsing regions - coord_t inset = - (loop_number < 0) ? 0 : - (loop_number == 0) ? - // one loop - ext_perimeter_spacing : - // two or more loops? - perimeter_spacing; - coord_t top_inset = inset; - - top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); - if(is_topmost_layer || is_bottom_layer) - inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); - else - inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset)))); - - // simplify infill contours according to resolution - Polygons pp; - for (ExPolygon& ex : infill_contour) - ex.simplify_p(m_scaled_resolution, &pp); - ExPolygons not_filled_exp = union_ex(pp); - // collapse too narrow infill areas - const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); - - ExPolygons infill_exp = offset2_ex( - not_filled_exp, - float(-min_perimeter_infill_spacing / 2.), - float(inset + min_perimeter_infill_spacing / 2.)); - // append infill areas to fill_surfaces - if (!top_expolygons.empty()) { - infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset))); - } - this->fill_surfaces->append(infill_exp, stInternal); - - apply_extra_perimeters(infill_exp); - - // BBS: get the no-overlap infill expolygons - { - ExPolygons polyWithoutOverlap; - polyWithoutOverlap = offset2_ex( - not_filled_exp, - float(-min_perimeter_infill_spacing / 2.), - float(+min_perimeter_infill_spacing / 2.)); - if (!top_expolygons.empty()) - polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons); - this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end()); - } - } -} - void PerimeterGenerator::process_classic() { group_region_by_fuzzify(*this); @@ -3187,6 +2683,603 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim } } +// ORCA: +// Inner Outer Inner wall ordering mode perimeter order optimisation functions +/** + * @brief Finds all perimeters touching a given set of reference lines, given as indexes. + * + * @param entities The list of PerimeterGeneratorArachneExtrusion entities. + * @param referenceIndices A set of indices representing the reference points. + * @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 + * @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ + * @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc). + * @return std::vector A vector of indices representing the touching perimeters. + */ +std::vector findAllTouchingPerimeters(const std::vector& entities, const std::unordered_set& referenceIndices, size_t threshold_external, size_t threshold_internal , size_t considered_inset_idx) { + std::unordered_set touchingIndices; + + for (const int refIdx : referenceIndices) { + const auto& referenceEntity = entities[refIdx]; + Points referencePoints = Arachne::to_points(*referenceEntity.extrusion); + for (size_t i = 0; i < entities.size(); ++i) { + // Skip already considered references and the reference entity + if (referenceIndices.count(i) > 0) continue; + const auto& entity = entities[i]; + if (entity.extrusion->inset_idx == 0) continue; // Ignore inset index 0 (external) perimeters from the re-ordering even if they are touching + + if (entity.extrusion->inset_idx != considered_inset_idx) { // Find Inset index perimeters that match the requested inset index + continue; // skip if they dont match + } + + Points points = Arachne::to_points(*entity.extrusion); + double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points); + // Add to touchingIndices if within threshold distance + size_t threshold=0; + if(referenceEntity.extrusion->inset_idx == 0) + threshold = threshold_external; + else + threshold = threshold_internal; + if (distance <= threshold) { + touchingIndices.insert(i); + } + } + } + return std::vector(touchingIndices.begin(), touchingIndices.end()); +} + +/** + * @brief Reorders perimeters based on proximity to the reference perimeter + * + * This approach finds all perimeters touching the external perimeter first and then finds all perimeters touching these new ones until none are left + * It ensures a level-by-level traversal, similar to BFS in graph theory. + * + * @param entities The list of PerimeterGeneratorArachneExtrusion entities. + * @param referenceIndex The index of the reference perimeter. + * @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 + * @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ + * @return std::vector The reordered list of perimeters based on proximity. + */ +std::vector reorderPerimetersByProximity(std::vector entities, size_t threshold_external, size_t threshold_internal) { + std::vector reordered; + std::unordered_set includedIndices; + + // Function to reorder perimeters starting from a given reference index + auto reorderFromReference = [&](int referenceIndex) { + std::unordered_set firstLevelIndices; + firstLevelIndices.insert(referenceIndex); + + // Find first level touching perimeters + std::vector firstLevelTouchingIndices = findAllTouchingPerimeters(entities, firstLevelIndices, threshold_external, threshold_internal, 1); + // Bring the largest first level perimeter to the front + // The longest first neighbour is most likely the dominant proximate perimeter + // hence printing it immediately after the external perimeter should speed things up + if (!firstLevelTouchingIndices.empty()) { + auto maxIt = std::max_element(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end(), [&entities](int a, int b) { + return entities[a].extrusion->getLength() < entities[b].extrusion->getLength(); + }); + std::iter_swap(maxIt, firstLevelTouchingIndices.end() - 1); + } + // Insert first level perimeters into reordered list + reordered.push_back(entities[referenceIndex]); + includedIndices.insert(referenceIndex); + + for (int idx : firstLevelTouchingIndices) { + if (includedIndices.count(idx) == 0) { + reordered.push_back(entities[idx]); + includedIndices.insert(idx); + } + } + + // Loop through all inset indices above 1 + size_t currentInsetIndex = 2; + while (true) { + std::unordered_set currentLevelIndices(firstLevelTouchingIndices.begin(), firstLevelTouchingIndices.end()); + std::vector currentLevelTouchingIndices = findAllTouchingPerimeters(entities, currentLevelIndices, threshold_external, threshold_internal, currentInsetIndex); + + // Break if no more touching perimeters are found + if (currentLevelTouchingIndices.empty()) { + break; + } + + // Exclude any already included indices from the current level touching indices + currentLevelTouchingIndices.erase( + std::remove_if(currentLevelTouchingIndices.begin(), currentLevelTouchingIndices.end(), + [&](int idx) { return includedIndices.count(idx) > 0; }), + currentLevelTouchingIndices.end()); + + // Bring the largest current level perimeter to the end + if (!currentLevelTouchingIndices.empty()) { + auto maxIt = std::max_element(currentLevelTouchingIndices.begin(), currentLevelTouchingIndices.end(), [&entities](int a, int b) { + return entities[a].extrusion->getLength() < entities[b].extrusion->getLength(); + }); + std::iter_swap(maxIt, currentLevelTouchingIndices.begin()); + } + + // Insert current level perimeters into reordered list + for (int idx : currentLevelTouchingIndices) { + if (includedIndices.count(idx) == 0) { + reordered.push_back(entities[idx]); + includedIndices.insert(idx); + } + } + + // Prepare for the next level + firstLevelTouchingIndices = currentLevelTouchingIndices; + currentInsetIndex++; + } + }; + + // Loop through all perimeters and reorder starting from each inset index 0 perimeter + for (size_t refIdx = 0; refIdx < entities.size(); ++refIdx) { + if (entities[refIdx].extrusion->inset_idx == 0 && includedIndices.count(refIdx) == 0) { + reorderFromReference(refIdx); + } + } + + // Append any remaining entities that were not included + for (size_t i = 0; i < entities.size(); ++i) { + if (includedIndices.count(i) == 0) { + reordered.push_back(entities[i]); + } + } + + return reordered; +} + +/** + * @brief Reorders the vector to bring external perimeter (i.e. paths with inset index 0) that are also contours (i.e. external facing lines) to the front. + * + * This function uses a stable partition to move all external perimeter contour elements to the front of the vector, + * while maintaining the relative order of non-contour elements. + * + * @param ordered_extrusions The vector of PerimeterGeneratorArachneExtrusion to reorder. + */ +void bringContoursToFront(std::vector& ordered_extrusions) { + std::stable_partition(ordered_extrusions.begin(), ordered_extrusions.end(), [](const PerimeterGeneratorArachneExtrusion& extrusion) { + return (extrusion.extrusion->is_contour() && extrusion.extrusion->inset_idx==0); + }); +} +// ORCA: +// Inner Outer Inner wall ordering mode perimeter order optimisation functions ended + + +// Thanks, Cura developers, for implementing an algorithm for generating perimeters with variable width (Arachne) that is based on the paper +// "A framework for adaptive width control of dense contour-parallel toolpaths in fused deposition modeling" +void PerimeterGenerator::process_arachne() +{ + group_region_by_fuzzify(*this); + + // other perimeters + m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); + coord_t perimeter_spacing = this->perimeter_flow.scaled_spacing(); + + // external perimeters + m_ext_mm3_per_mm = this->ext_perimeter_flow.mm3_per_mm(); + coord_t ext_perimeter_width = this->ext_perimeter_flow.scaled_width(); + coord_t ext_perimeter_spacing = this->ext_perimeter_flow.scaled_spacing(); + coord_t ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.spacing() + this->perimeter_flow.spacing())); + // overhang perimeters + m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); + + // solid infill + coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); + + // prepare grown lower layer slices for overhang detection + if (this->lower_slices != nullptr && this->config->detect_overhang_wall) { + // We consider overhang any part where the entire nozzle diameter is not supported by the + // lower layer, so we take lower slices and offset them by half the nozzle diameter used + // in the current layer + double nozzle_diameter = this->print_config->nozzle_diameter.get_at(this->config->wall_filament - 1); + m_lower_slices_polygons = offset(*this->lower_slices, float(scale_(+nozzle_diameter / 2))); + } + + Surfaces all_surfaces = this->slices->surfaces; + + process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width); + // BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled + double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution; + // we need to process each island separately because we might have different + // extra perimeters for each one + for (const Surface& surface : all_surfaces) { + coord_t bead_width_0 = ext_perimeter_spacing; + // detect how many perimeters must be generated for this island + int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops + int sparse_infill_density = this->config->sparse_infill_density.value; + if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall + loop_number++; + + // Set the bottommost layer to be one wall + const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false; + if (is_bottom_layer && this->config->only_one_wall_first_layer) + loop_number = 0; + + // Orca: set the topmost layer to be one wall according to the config + const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false; + if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top) + loop_number = 0; + + auto apply_precise_outer_wall = config->precise_outer_wall && this->config->wall_sequence == WallSequence::InnerOuter; + // Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled. + ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution), + apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing ) + : -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); + + Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config); + // Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity. + input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false; + + coord_t wall_0_inset = 0; + if (apply_precise_outer_wall) + wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); + + //PS: One wall top surface for Arachne + ExPolygons top_expolygons; + // Calculate how many inner loops remain when TopSurfaces is selected. + const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1; + + // Set one perimeter when TopSurfaces is selected. + if (config->only_one_wall_top) + loop_number = 0; + + Arachne::WallToolPathsParams input_params_tmp = input_params; + + Polygons last_p = to_polygons(last); + Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1), + wall_0_inset, layer_height, input_params_tmp); + std::vector perimeters = wallToolPaths.getToolPaths(); + ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour()); + + // Check if there are some remaining perimeters to generate (the number of perimeters + // is greater than one together with enabled the single perimeter on top surface feature). + if (inner_loop_number >= 0) { + assert(upper_slices != nullptr); + + // Infill contour bounding box. + BoundingBox infill_contour_bbox = get_extents(infill_contour); + infill_contour_bbox.offset(SCALED_EPSILON); + + coord_t perimeter_width = this->perimeter_flow.scaled_width(); + + // Get top ExPolygons from current infill contour. + Polygons upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox); + top_expolygons = diff_ex(infill_contour, upper_slices_clipped); + + if (!top_expolygons.empty()) { + if (lower_slices != nullptr) { + const float bridge_offset = float(std::max(ext_perimeter_spacing, perimeter_width)); + const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, infill_contour_bbox); + const ExPolygons current_slices_bridges = offset_ex(diff_ex(top_expolygons, lower_slices_clipped), bridge_offset); + + // Remove bridges from top surface polygons. + top_expolygons = diff_ex(top_expolygons, current_slices_bridges); + } + + // Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line. + // ORCA: skip if the top surface area is smaller than "min_width_top_surface" + const float top_surface_min_width = std::max(float(ext_perimeter_spacing) / 4.f + scaled(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f); + // Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little. + // ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount, + // not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions + // due to thin lines being generated + top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85)); + + // Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges). + const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons); + + // Get final top ExPolygons. + top_expolygons = intersection_ex(top_expolygons, infill_contour); + + const Polygons not_top_polygons = to_polygons(not_top_expolygons); + Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp); + std::vector inner_perimeters = inner_wall_tool_paths.getToolPaths(); + + // Recalculate indexes of inner perimeters before merging them. + if (!perimeters.empty()) { + for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) { + if (inner_perimeter.empty()) + continue; + for (Arachne::ExtrusionLine &el : inner_perimeter) + ++el.inset_idx; + } + } + + perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end()); + infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour()); + } else { + // There is no top surface ExPolygon, so we call Arachne again with parameters + // like when the single perimeter feature is disabled. + Arachne::WallToolPaths no_single_perimeter_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp); + perimeters = no_single_perimeter_tool_paths.getToolPaths(); + infill_contour = union_ex(no_single_perimeter_tool_paths.getInnerContour()); + } + } + //PS + + loop_number = int(perimeters.size()) - 1; + + #ifdef ARACHNE_DEBUG + { + static int iRun = 0; + export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour())); + } +#endif + + // All closed ExtrusionLine should have the same the first and the last point. + // But in rare cases, Arachne produce ExtrusionLine marked as closed but without + // equal the first and the last point. + assert([&perimeters = std::as_const(perimeters)]() -> bool { + for (const Arachne::VariableWidthLines& perimeter : perimeters) + for (const Arachne::ExtrusionLine& el : perimeter) + if (el.is_closed && el.junctions.front().p != el.junctions.back().p) + return false; + return true; + }()); + + int start_perimeter = int(perimeters.size()) - 1; + int end_perimeter = -1; + int direction = -1; + + bool is_outer_wall_first = + this->config->wall_sequence == WallSequence::OuterInner || + this->config->wall_sequence == WallSequence::InnerOuterInner; + + if (layer_id == 0){ // disable inner outer inner algorithm after the first layer + is_outer_wall_first = + this->config->wall_sequence == WallSequence::OuterInner; + } + if (is_outer_wall_first) { + start_perimeter = 0; + end_perimeter = int(perimeters.size()); + direction = 1; + } + + std::vector all_extrusions; + for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) { + if (perimeters[perimeter_idx].empty()) + continue; + for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx]) + all_extrusions.emplace_back(&wall); + } + + // 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; + 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, is_outer_wall_first); + for (auto [before, after] : extrusions_constrains) { + auto after_it = map_extrusion_to_idx.find(after); + ++blocked[after_it->second]; + blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second); + } + + std::vector processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed. + Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position. + std::vector ordered_extrusions; // To store our result in. At the end we'll std::swap. + ordered_extrusions.reserve(all_extrusions.size()); + + while (ordered_extrusions.size() < all_extrusions.size()) { + size_t best_candidate = 0; + double best_distance_sqr = std::numeric_limits::max(); + bool is_best_closed = false; + + std::vector available_candidates; + for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) { + if (processed[candidate] || blocked[candidate]) + continue; // Not a valid candidate. + available_candidates.push_back(candidate); + } + + std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { + return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; + }); + + for (const size_t candidate_path_idx : available_candidates) { + auto& path = all_extrusions[candidate_path_idx]; + + if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end. + if (best_distance_sqr == std::numeric_limits::max()) { + best_candidate = candidate_path_idx; + is_best_closed = path->is_closed; + } + continue; + } + + const Point candidate_position = path->junctions.front().p; + double distance_sqr = (current_position - candidate_position).cast().norm(); + if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far. + if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits::max()) || (!path->is_closed && !is_best_closed)) { + best_candidate = candidate_path_idx; + best_distance_sqr = distance_sqr; + is_best_closed = path->is_closed; + } + } + } + + auto& best_path = all_extrusions[best_candidate]; + ordered_extrusions.push_back({ best_path, best_path->is_contour() }); + processed[best_candidate] = true; + for (size_t unlocked_idx : blocking[best_candidate]) + blocked[unlocked_idx]--; + + if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then. + if (best_path->is_closed) + current_position = best_path->junctions[0].p; //We end where we started. + else + current_position = best_path->junctions.back().p; //Pick the other end from where we started. + } + } + + // printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer + if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer + if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering + int position = 0; // index to run the re-ordering for multiple external perimeters in a single island. + int arr_i, arr_j = 0; // indexes to run through the walls in the for loops + int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values + + // To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering + // for OI mode that is used the basis for IOI + bringContoursToFront(ordered_extrusions); + std::vector reordered_extrusions; + + // Debug statement to print spacing values: + //printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing()); + + // Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by 3% to cover + // rounding errors + coord_t threshold_external = (this->ext_perimeter_flow.scaled_spacing()/2 + this->perimeter_flow.scaled_spacing()/2)*1.03; + + // For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by 3% to cover rounding errors. + coord_t threshold_internal = this->perimeter_flow.scaled_spacing() * 1.03; + + // Re-order extrusions based on distance + // Alorithm will aggresively optimise for the appearance of the outermost perimeter + ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal ); + reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list. + + // Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis + // scan to find the external perimeter, first internal, second internal and last perimeter in the island. + // We then advance the position index to move to the second island and continue until there are no more + // perimeters left. + while (position < reordered_extrusions.size()) { + outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1 + max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list + // run through the walls to get the index values that need re-ordering until the first one for each + // is found. Start at "position" index to enable the for loop to iterate for multiple external + // perimeters in a single island + // printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal); + for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { + // printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i); + switch (reordered_extrusions[arr_i].extrusion->inset_idx) { + case 0: // external perimeter + if (outer == -1) + outer = arr_i; + break; + case 1: // first internal wall + if (first_internal==-1 && arr_i>outer && outer!=-1){ + first_internal = arr_i; + } + break; + case 2: // second internal wall + if (second_internal == -1 && arr_i > first_internal && outer!=-1){ + second_internal = arr_i; + } + break; + } + if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order. + // This means we entered a new island. + arr_i=arr_i-1; //step back one perimeter + max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island. + break; // exit the for loop + } + } + + // printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal); + if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in. + std::vector inner_outer_extrusions; // temporary array to hold extrusions for reordering + inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations + // printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position); + + for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2 + if(arr_j >= second_internal){ + //printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j); + inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j]; + current_perimeter++; + } + } + + for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order + // printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1); + inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j]; + } + + for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array + ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position]; + } + // go to the next perimeter from the current position to continue scanning for external walls in the same island + position = arr_i + 1; + } + } + } + + bool steep_overhang_contour = false; + bool steep_overhang_hole = false; + const WallDirection wall_direction = config->wall_direction; + if (wall_direction != WallDirection::Auto) { + // Skip steep overhang detection if wall direction is specified + steep_overhang_contour = true; + steep_overhang_hole = true; + } + if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { + // All walls are counter-clockwise initially, so we don't need to reorient it if that's what we want + if (wall_direction != WallDirection::CounterClockwise) { + reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, + // Reverse internal only if the wall direction is auto + this->config->overhang_reverse_internal_only && wall_direction == WallDirection::Auto); + } + this->loops->append(extrusion_coll); + } + + const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing; + + if (offset_ex(infill_contour, -float(spacing / 2.)).empty()) + infill_contour.clear(); // Infill region is too small, so let's filter it out. + + // create one more offset to be used as boundary for fill + // we offset by half the perimeter spacing (to get to the actual infill boundary) + // and then we offset back and forth by half the infill spacing to only consider the + // non-collapsing regions + coord_t inset = + (loop_number < 0) ? 0 : + (loop_number == 0) ? + // one loop + ext_perimeter_spacing : + // two or more loops? + perimeter_spacing; + coord_t top_inset = inset; + + top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); + if(is_topmost_layer || is_bottom_layer) + inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset)))); + else + inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset)))); + + // simplify infill contours according to resolution + Polygons pp; + for (ExPolygon& ex : infill_contour) + ex.simplify_p(m_scaled_resolution, &pp); + ExPolygons not_filled_exp = union_ex(pp); + // collapse too narrow infill areas + const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); + + ExPolygons infill_exp = offset2_ex( + not_filled_exp, + float(-min_perimeter_infill_spacing / 2.), + float(inset + min_perimeter_infill_spacing / 2.)); + // append infill areas to fill_surfaces + if (!top_expolygons.empty()) { + infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset))); + } + this->fill_surfaces->append(infill_exp, stInternal); + + apply_extra_perimeters(infill_exp); + + // BBS: get the no-overlap infill expolygons + { + ExPolygons polyWithoutOverlap; + polyWithoutOverlap = offset2_ex( + not_filled_exp, + float(-min_perimeter_infill_spacing / 2.), + float(+min_perimeter_infill_spacing / 2.)); + if (!top_expolygons.empty()) + polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons); + this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end()); + } + } +} + bool PerimeterGeneratorLoop::is_internal_contour() const { // An internal contour is a contour containing no other contours