diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp index b57c84d6398..5ade0c92070 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.cpp @@ -1,10 +1,8 @@ //Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. -#include - #include "BeadingStrategy.hpp" -#include "Point.hpp" +#include "libslic3r/Point.hpp" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp index 99e38239f9d..0a08f5d2d01 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp @@ -4,9 +4,13 @@ #ifndef BEADING_STRATEGY_H #define BEADING_STRATEGY_H +#include #include +#include +#include +#include -#include "../../libslic3r.h" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp index 97acd271ac3..d6cb1331c11 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.cpp @@ -3,51 +3,55 @@ #include "BeadingStrategyFactory.hpp" +#include +#include +#include + #include "LimitedBeadingStrategy.hpp" #include "WideningBeadingStrategy.hpp" #include "DistributedBeadingStrategy.hpp" #include "RedistributeBeadingStrategy.hpp" #include "OuterWallInsetBeadingStrategy.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" -#include -#include +namespace Slic3r::Arachne { -namespace Slic3r::Arachne +BeadingStrategyPtr BeadingStrategyFactory::makeStrategy(const coord_t preferred_bead_width_outer, + const coord_t preferred_bead_width_inner, + const coord_t preferred_transition_length, + const float transitioning_angle, + const bool print_thin_walls, + const coord_t min_bead_width, + const coord_t min_feature_size, + const double wall_split_middle_threshold, + const double wall_add_middle_threshold, + const coord_t max_bead_count, + const coord_t outer_wall_offset, + const int inward_distributed_center_wall_count, + const double minimum_variable_line_ratio) { + // Handle a special case when there is just one external perimeter. + // Because big differences in bead width for inner and other perimeters cause issues with current beading strategies. + const coord_t optimal_width = max_bead_count <= 2 ? preferred_bead_width_outer : preferred_bead_width_inner; + BeadingStrategyPtr ret = std::make_unique(optimal_width, preferred_transition_length, transitioning_angle, + wall_split_middle_threshold, wall_add_middle_threshold, + inward_distributed_center_wall_count); -BeadingStrategyPtr BeadingStrategyFactory::makeStrategy( - const coord_t preferred_bead_width_outer, - const coord_t preferred_bead_width_inner, - const coord_t preferred_transition_length, - const float transitioning_angle, - const bool print_thin_walls, - const coord_t min_bead_width, - const coord_t min_feature_size, - const double wall_split_middle_threshold, - const double wall_add_middle_threshold, - const coord_t max_bead_count, - const coord_t outer_wall_offset, - const int inward_distributed_center_wall_count, - const double minimum_variable_line_ratio -) -{ - BeadingStrategyPtr ret = std::make_unique(preferred_bead_width_inner, preferred_transition_length, transitioning_angle, wall_split_middle_threshold, wall_add_middle_threshold, inward_distributed_center_wall_count); - BOOST_LOG_TRIVIAL(debug) << "Applying the Redistribute meta-strategy with outer-wall width = " << preferred_bead_width_outer << ", inner-wall width = " << preferred_bead_width_inner << "."; + BOOST_LOG_TRIVIAL(trace) << "Applying the Redistribute meta-strategy with outer-wall width = " << preferred_bead_width_outer << ", inner-wall width = " << preferred_bead_width_inner << "."; ret = std::make_unique(preferred_bead_width_outer, minimum_variable_line_ratio, std::move(ret)); if (print_thin_walls) { - BOOST_LOG_TRIVIAL(debug) << "Applying the Widening Beading meta-strategy with minimum input width " << min_feature_size << " and minimum output width " << min_bead_width << "."; + BOOST_LOG_TRIVIAL(trace) << "Applying the Widening Beading meta-strategy with minimum input width " << min_feature_size << " and minimum output width " << min_bead_width << "."; ret = std::make_unique(std::move(ret), min_feature_size, min_bead_width); } // Orca: we allow negative outer_wall_offset here - if (outer_wall_offset != 0) - { - BOOST_LOG_TRIVIAL(debug) << "Applying the OuterWallOffset meta-strategy with offset = " << outer_wall_offset << "."; + if (outer_wall_offset != 0) { + BOOST_LOG_TRIVIAL(trace) << "Applying the OuterWallOffset meta-strategy with offset = " << outer_wall_offset << "."; ret = std::make_unique(outer_wall_offset, std::move(ret)); } - //Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. - BOOST_LOG_TRIVIAL(debug) << "Applying the Limited Beading meta-strategy with maximum bead count = " << max_bead_count << "."; + // Apply the LimitedBeadingStrategy last, since that adds a 0-width marker wall which other beading strategies shouldn't touch. + BOOST_LOG_TRIVIAL(trace) << "Applying the Limited Beading meta-strategy with maximum bead count = " << max_bead_count << "."; ret = std::make_unique(max_bead_count, std::move(ret)); return ret; } diff --git a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp index a586906f459..e24a85e5c55 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp @@ -4,8 +4,12 @@ #ifndef BEADING_STRATEGY_FACTORY_H #define BEADING_STRATEGY_FACTORY_H +#include +#include + #include "BeadingStrategy.hpp" #include "../../Point.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp index c8a84c40113..7234015ae29 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp @@ -1,7 +1,12 @@ // Copyright (c) 2022 Ultimaker B.V. // CuraEngine is released under the terms of the AGPLv3 or higher. #include +#include +#include +#include + #include "DistributedBeadingStrategy.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp index 4d651732d4a..991d5028b70 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.hpp @@ -5,6 +5,7 @@ #define DISTRIBUTED_BEADING_STRATEGY_H #include "BeadingStrategy.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp index 97d854b418e..24e04c3f26d 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.cpp @@ -1,11 +1,14 @@ //Copyright (c) 2022 Ultimaker B.V. //CuraEngine is released under the terms of the AGPLv3 or higher. -#include #include +#include +#include +#include #include "LimitedBeadingStrategy.hpp" -#include "Point.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp index 33292bc09fc..166057f03ed 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/LimitedBeadingStrategy.hpp @@ -4,7 +4,10 @@ #ifndef LIMITED_BEADING_STRATEGY_H #define LIMITED_BEADING_STRATEGY_H +#include + #include "BeadingStrategy.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp index 1406f7daa8b..ac9edb1dba6 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.cpp @@ -4,6 +4,9 @@ #include "OuterWallInsetBeadingStrategy.hpp" #include +#include + +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp index 45a700b02e9..f0b4622df3d 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/OuterWallInsetBeadingStrategy.hpp @@ -4,7 +4,10 @@ #ifndef OUTER_WALL_INSET_BEADING_STRATEGY_H #define OUTER_WALL_INSET_BEADING_STRATEGY_H +#include + #include "BeadingStrategy.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp index 2b4dda02722..7307781840d 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.cpp @@ -5,6 +5,9 @@ #include #include +#include + +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp index f0fefe23899..701aa630413 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/RedistributeBeadingStrategy.hpp @@ -4,7 +4,10 @@ #ifndef REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H #define REDISTRIBUTE_DISTRIBUTED_BEADING_STRATEGY_H +#include + #include "BeadingStrategy.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp index eefcab8e7b0..4c6dbb27061 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp @@ -3,6 +3,11 @@ #include "WideningBeadingStrategy.hpp" +#include +#include + +#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" + namespace Slic3r::Arachne { @@ -24,17 +29,16 @@ WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickn if (thickness < optimal_width) { Beading ret; ret.total_thickness = thickness; - if (thickness >= min_input_width) - { + if (thickness >= min_input_width) { ret.bead_widths.emplace_back(std::max(thickness, min_output_width)); ret.toolpath_locations.emplace_back(thickness / 2); - } else { + ret.left_over = 0; + } else ret.left_over = thickness; - } + return ret; - } else { + } else return parent->compute(thickness, bead_count); - } } coord_t WideningBeadingStrategy::getOptimalThickness(coord_t bead_count) const diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp index 3e799b9af76..225aeed88b0 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.hpp @@ -4,7 +4,11 @@ #ifndef WIDENING_BEADING_STRATEGY_H #define WIDENING_BEADING_STRATEGY_H +#include +#include + #include "BeadingStrategy.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/PerimeterOrder.cpp b/src/libslic3r/Arachne/PerimeterOrder.cpp new file mode 100644 index 00000000000..c6c2755a48d --- /dev/null +++ b/src/libslic3r/Arachne/PerimeterOrder.cpp @@ -0,0 +1,280 @@ +#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 new file mode 100644 index 00000000000..f8469d917f1 --- /dev/null +++ b/src/libslic3r/Arachne/PerimeterOrder.hpp @@ -0,0 +1,51 @@ +#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/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 19d49c3e12f..2d2cf9896db 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -3,21 +3,27 @@ #include "SkeletalTrapezoidation.hpp" -#include -#include -#include -#include -#include #include - -#include "utils/linearAlg2D.hpp" -#include "Utils.hpp" -#include "SVG.hpp" -#include "Geometry/VoronoiVisualUtils.hpp" -#include "Geometry/VoronoiUtilsCgal.hpp" -#include "../EdgeGrid.hpp" - -#include "Geometry/VoronoiUtils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Geometry/VoronoiUtils.hpp" +#include "ankerl/unordered_dense.h" +#include "libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp" +#include "libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp" +#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp" +#include "libslic3r/Arachne/utils/ExtrusionLine.hpp" + +#ifndef NDEBUG + #include "libslic3r/EdgeGrid.hpp" +#endif #define SKELETAL_TRAPEZOIDATION_BEAD_SEARCH_MAX 1000 //A limit to how long it'll keep searching for adjacent beads. Increasing will re-use beadings more often (saving performance), but search longer for beading (costing performance). @@ -104,7 +110,7 @@ SkeletalTrapezoidation::node_t &SkeletalTrapezoidation::makeNode(const VD::verte } } -void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_type &vd_edge, edge_t *&prev_edge, Point &start_source_point, Point &end_source_point, const std::vector &segments) { +void SkeletalTrapezoidation::transferEdge(const Point &from, const Point &to, const VD::edge_type &vd_edge, edge_t *&prev_edge, const Point &start_source_point, const Point &end_source_point, const std::vector &segments) { auto he_edge_it = vd_edge_to_he_edge.find(vd_edge.twin()); if (he_edge_it != vd_edge_to_he_edge.end()) { // Twin segment(s) have already been made @@ -152,8 +158,7 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_t assert(twin->prev->twin); // Back rib assert(twin->prev->twin->prev); // Prev segment along parabola - constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped - graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + graph.makeRib(prev_edge, start_source_point, end_source_point); } assert(prev_edge); } @@ -203,10 +208,8 @@ void SkeletalTrapezoidation::transferEdge(Point from, Point to, const VD::edge_t p0 = p1; v0 = v1; - if (p1_idx < discretized.size() - 1) - { // Rib for last segment gets introduced outside this function! - constexpr bool is_not_next_to_start_or_end = false; // Only ribs at the end of a cell should be skipped - graph.makeRib(prev_edge, start_source_point, end_source_point, is_not_next_to_start_or_end); + if (p1_idx < discretized.size() - 1) { // Rib for last segment gets introduced outside this function! + graph.makeRib(prev_edge, start_source_point, end_source_point); } } assert(prev_edge); @@ -326,50 +329,6 @@ Points SkeletalTrapezoidation::discretize(const VD::edge_type& vd_edge, const st } } -bool SkeletalTrapezoidation::computePointCellRange(const VD::cell_type &cell, Point &start_source_point, Point &end_source_point, const VD::edge_type *&starting_vd_edge, const VD::edge_type *&ending_vd_edge, const std::vector &segments) { - if (cell.incident_edge()->is_infinite()) - return false; //Infinite edges only occur outside of the polygon. Don't copy any part of this cell. - - // Check if any point of the cell is inside or outside polygon - // Copy whole cell into graph or not at all - - // If the cell.incident_edge()->vertex0() is far away so much that it doesn't even fit into Vec2i64, then there is no way that it will be inside the input polygon. - if (const VD::vertex_type &vert = *cell.incident_edge()->vertex0(); - vert.x() >= double(std::numeric_limits::max()) || vert.x() <= double(std::numeric_limits::lowest()) || - vert.y() >= double(std::numeric_limits::max()) || vert.y() <= double(std::numeric_limits::lowest())) - return false; // Don't copy any part of this cell - - const Point source_point = Geometry::VoronoiUtils::get_source_point(cell, segments.begin(), segments.end()); - const PolygonsPointIndex source_point_index = Geometry::VoronoiUtils::get_source_point_index(cell, segments.begin(), segments.end()); - Vec2i64 some_point = Geometry::VoronoiUtils::to_point(cell.incident_edge()->vertex0()); - if (some_point == source_point.cast()) - some_point = Geometry::VoronoiUtils::to_point(cell.incident_edge()->vertex1()); - - //Test if the some_point is even inside the polygon. - //The edge leading out of a polygon must have an endpoint that's not in the corner following the contour of the polygon at that vertex. - //So if it's inside the corner formed by the polygon vertex, it's all fine. - //But if it's outside of the corner, it must be a vertex of the Voronoi diagram that goes outside of the polygon towards infinity. - if (!LinearAlg2D::isInsideCorner(source_point_index.prev().p(), source_point_index.p(), source_point_index.next().p(), some_point)) - return false; // Don't copy any part of this cell - - const VD::edge_type* vd_edge = cell.incident_edge(); - do { - assert(vd_edge->is_finite()); - if (Vec2i64 p1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()); p1 == source_point.cast()) { - start_source_point = source_point; - end_source_point = source_point; - starting_vd_edge = vd_edge->next(); - ending_vd_edge = vd_edge; - } else { - assert((Geometry::VoronoiUtils::to_point(vd_edge->vertex0()) == source_point.cast() || !vd_edge->is_secondary()) && "point cells must end in the point! They cannot cross the point with an edge, because collinear edges are not allowed in the input."); - } - } - while (vd_edge = vd_edge->next(), vd_edge != cell.incident_edge()); - assert(starting_vd_edge && ending_vd_edge); - assert(starting_vd_edge != ending_vd_edge); - return true; -} - SkeletalTrapezoidation::SkeletalTrapezoidation(const Polygons& polys, const BeadingStrategy& beading_strategy, double transitioning_angle, coord_t discretization_step_size, coord_t transition_filter_dist, coord_t allowed_filter_deviation, @@ -437,15 +396,20 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) // Compute and store result in above variables if (cell.contains_point()) { - const bool keep_going = computePointCellRange(cell, start_source_point, end_source_point, starting_voronoi_edge, ending_voronoi_edge, segments); - if (!keep_going) + Geometry::PointCellRange cell_range = Geometry::VoronoiUtils::compute_point_cell_range(cell, segments.cbegin(), segments.cend()); + start_source_point = cell_range.source_point; + end_source_point = cell_range.source_point; + starting_voronoi_edge = cell_range.edge_begin; + ending_voronoi_edge = cell_range.edge_end; + + if (!cell_range.is_valid()) continue; } else { assert(cell.contains_segment()); Geometry::SegmentCellRange cell_range = Geometry::VoronoiUtils::compute_segment_cell_range(cell, segments.cbegin(), segments.cend()); assert(cell_range.is_valid()); - start_source_point = cell_range.segment_start_point; - end_source_point = cell_range.segment_end_point; + start_source_point = cell_range.source_segment_start_point; + end_source_point = cell_range.source_segment_end_point; starting_voronoi_edge = cell_range.edge_begin; ending_voronoi_edge = cell_range.edge_end; } @@ -462,8 +426,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) node_t *starting_node = vd_node_to_he_node[starting_voronoi_edge->vertex0()]; starting_node->data.distance_to_boundary = 0; - constexpr bool is_next_to_start_or_end = true; - graph.makeRib(prev_edge, start_source_point, end_source_point, is_next_to_start_or_end); + graph.makeRib(prev_edge, start_source_point, end_source_point); for (const VD::edge_type* vd_edge = starting_voronoi_edge->next(); vd_edge != ending_voronoi_edge; vd_edge = vd_edge->next()) { assert(vd_edge->is_finite()); assert(Geometry::VoronoiUtils::is_in_range(*vd_edge)); @@ -471,7 +434,7 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) Point v1 = Geometry::VoronoiUtils::to_point(vd_edge->vertex0()).cast(); Point v2 = Geometry::VoronoiUtils::to_point(vd_edge->vertex1()).cast(); transferEdge(v1, v2, *vd_edge, prev_edge, start_source_point, end_source_point, segments); - graph.makeRib(prev_edge, start_source_point, end_source_point, vd_edge->next() == ending_voronoi_edge); + graph.makeRib(prev_edge, start_source_point, end_source_point); } transferEdge(Geometry::VoronoiUtils::to_point(ending_voronoi_edge->vertex0()).cast(), end_source_point, *ending_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); @@ -493,6 +456,8 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) edge.from->incident_edge = &edge; } +using NodeSet = SkeletalTrapezoidation::NodeSet; + void SkeletalTrapezoidation::separatePointyQuadEndNodes() { NodeSet visited_nodes; @@ -1968,6 +1933,8 @@ void SkeletalTrapezoidation::addToolpathSegment(const ExtrusionJunction& from, c void SkeletalTrapezoidation::connectJunctions(ptr_vector_t& edge_junctions) { + using EdgeSet = ankerl::unordered_dense::set; + EdgeSet unprocessed_quad_starts(graph.edges.size() * 5 / 2); for (edge_t& edge : graph.edges) { diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index ca8e13473f5..70c2a7d9a3d 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -5,11 +5,11 @@ #define SKELETAL_TRAPEZOIDATION_H #include - +#include #include // smart pointers #include // pair - -#include +#include +#include #include "utils/HalfEdgeGraph.hpp" #include "utils/PolygonsSegmentIndex.hpp" @@ -20,6 +20,10 @@ #include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" #include "SkeletalTrapezoidationGraph.hpp" #include "../Geometry/Voronoi.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/libslic3r.h" //#define ARACHNE_DEBUG //#define ARACHNE_DEBUG_VORONOI @@ -82,10 +86,7 @@ class SkeletalTrapezoidation public: using Segment = PolygonsSegmentIndex; - using NodeSet = ankerl::unordered_dense::set; - using EdgeSet = ankerl::unordered_dense::set; - using EdgeMap = ankerl::unordered_dense::map; - using NodeMap = ankerl::unordered_dense::map; + using NodeSet = ankerl::unordered_dense::set; /*! * Construct a new trapezoidation problem to solve. @@ -169,8 +170,8 @@ class SkeletalTrapezoidation * mapping each voronoi VD edge to the corresponding halfedge HE edge * In case the result segment is discretized, we map the VD edge to the *last* HE edge */ - EdgeMap vd_edge_to_he_edge; - NodeMap vd_node_to_he_node; + ankerl::unordered_dense::map vd_edge_to_he_edge; + ankerl::unordered_dense::map vd_node_to_he_node; node_t &makeNode(const VD::vertex_type &vd_node, Point p); //!< Get the node which the VD node maps to, or create a new mapping if there wasn't any yet. /*! @@ -182,7 +183,7 @@ class SkeletalTrapezoidation * Transfer an edge from the VD to the HE and perform discretization of parabolic edges (and vertex-vertex edges) * \p prev_edge serves as input and output. May be null as input. */ - void transferEdge(Point from, Point to, const VD::edge_type &vd_edge, edge_t *&prev_edge, Point &start_source_point, Point &end_source_point, const std::vector &segments); + void transferEdge(const Point &from, const Point &to, const VD::edge_type &vd_edge, edge_t *&prev_edge, const Point &start_source_point, const Point &end_source_point, const std::vector &segments); /*! * Discretize a Voronoi edge that represents the medial axis of a vertex- @@ -211,32 +212,6 @@ class SkeletalTrapezoidation */ Points discretize(const VD::edge_type& segment, const std::vector& segments); - /*! - * Compute the range of line segments that surround a cell of the skeletal - * graph that belongs to a point on the medial axis. - * - * This should only be used on cells that belong to a corner in the skeletal - * graph, e.g. triangular cells, not trapezoid cells. - * - * The resulting line segments is just the first and the last segment. They - * are linked to the neighboring segments, so you can iterate over the - * segments until you reach the last segment. - * \param cell The cell to compute the range of line segments for. - * \param[out] start_source_point The start point of the source segment of - * this cell. - * \param[out] end_source_point The end point of the source segment of this - * cell. - * \param[out] starting_vd_edge The edge of the Voronoi diagram where the - * loop around the cell starts. - * \param[out] ending_vd_edge The edge of the Voronoi diagram where the loop - * around the cell ends. - * \param points All vertices of the input Polygons. - * \param segments All edges of the input Polygons. - * /return Whether the cell is inside of the polygon. If it's outside of the - * polygon we should skip processing it altogether. - */ - static bool computePointCellRange(const VD::cell_type &cell, Point &start_source_point, Point &end_source_point, const VD::edge_type *&starting_vd_edge, const VD::edge_type *&ending_vd_edge, const std::vector &segments); - /*! * For VD cells associated with an input polygon vertex, we need to separate the node at the end and start of the cell into two * That way we can reach both the quad_start and the quad_end from the [incident_edge] of the two new nodes diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp index c49340ec596..f7a30c32db8 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.cpp @@ -2,14 +2,18 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include "SkeletalTrapezoidationGraph.hpp" -#include "../Line.hpp" -#include - +#include #include +#include +#include +#include +#include -#include "utils/linearAlg2D.hpp" #include "../Line.hpp" +#include "libslic3r/Arachne/SkeletalTrapezoidationEdge.hpp" +#include "libslic3r/Arachne/SkeletalTrapezoidationJoint.hpp" +#include "libslic3r/Point.hpp" namespace Slic3r::Arachne { @@ -182,8 +186,8 @@ bool STHalfEdgeNode::isLocalMaximum(bool strict) const void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) { - ankerl::unordered_dense::map::iterator> edge_locator; - ankerl::unordered_dense::map::iterator> node_locator; + ankerl::unordered_dense::map edge_locator; + ankerl::unordered_dense::map node_locator; for (auto edge_it = edges.begin(); edge_it != edges.end(); ++edge_it) { @@ -195,7 +199,7 @@ void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) node_locator.emplace(&*node_it, node_it); } - auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, std::list::iterator& current_edge_it, bool& edge_it_is_updated) + auto safelyRemoveEdge = [this, &edge_locator](edge_t* to_be_removed, Edges::iterator& current_edge_it, bool& edge_it_is_updated) { if (current_edge_it != edges.end() && to_be_removed == &*current_edge_it) @@ -315,8 +319,7 @@ void SkeletalTrapezoidationGraph::collapseSmallEdges(coord_t snap_dist) } } -void SkeletalTrapezoidationGraph::makeRib(edge_t*& prev_edge, Point start_source_point, Point end_source_point, bool is_next_to_start_or_end) -{ +void SkeletalTrapezoidationGraph::makeRib(edge_t *&prev_edge, const Point &start_source_point, const Point &end_source_point) { Point p; Line(start_source_point, end_source_point).distance_to_infinite_squared(prev_edge->to->p, &p); coord_t dist = (prev_edge->to->p - p).cast().norm(); diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp index cfdbfecdafc..5baac24accb 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidationGraph.hpp @@ -5,10 +5,20 @@ #define SKELETAL_TRAPEZOIDATION_GRAPH_H #include +#include #include "utils/HalfEdgeGraph.hpp" #include "SkeletalTrapezoidationEdge.hpp" #include "SkeletalTrapezoidationJoint.hpp" +#include "libslic3r/Arachne/utils/HalfEdge.hpp" +#include "libslic3r/Arachne/utils/HalfEdgeNode.hpp" +#include "libslic3r/libslic3r.h" + +namespace Slic3r +{ +class Line; +class Point; +}; namespace Slic3r::Arachne { @@ -83,7 +93,7 @@ class SkeletalTrapezoidationGraph: public HalfEdgeGraph small_holes; + Polygons small_holes; for (auto it = thiss.begin(); it < new_end;) { if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) { if (area >= 0) { @@ -782,98 +782,4 @@ 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 457f7e71494..00652fc0915 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -90,16 +90,6 @@ class WallToolPaths 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/ExtrusionJunction.cpp b/src/libslic3r/Arachne/utils/ExtrusionJunction.cpp deleted file mode 100644 index 3cdfa0d8d20..00000000000 --- a/src/libslic3r/Arachne/utils/ExtrusionJunction.cpp +++ /dev/null @@ -1,18 +0,0 @@ -//Copyright (c) 2020 Ultimaker B.V. -//CuraEngine is released under the terms of the AGPLv3 or higher. - -#include "ExtrusionJunction.hpp" - -namespace Slic3r::Arachne -{ - -bool ExtrusionJunction::operator ==(const ExtrusionJunction& other) const -{ - return p == other.p - && w == other.w - && perimeter_index == other.perimeter_index; -} - -ExtrusionJunction::ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index) : p(p), w(w), perimeter_index(perimeter_index) {} - -} diff --git a/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp b/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp index 942f828b3b1..7789612e4b4 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionJunction.hpp @@ -37,9 +37,11 @@ struct ExtrusionJunction */ size_t perimeter_index; - ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index); + ExtrusionJunction(const Point p, const coord_t w, const coord_t perimeter_index) : p(p), w(w), perimeter_index(perimeter_index) {} - bool operator==(const ExtrusionJunction& other) const; + bool operator==(const ExtrusionJunction &other) const { + return p == other.p && w == other.w && perimeter_index == other.perimeter_index; + } coord_t x() const { return p.x(); } coord_t y() const { return p.y(); } diff --git a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp index 35233f10751..49449cc59d0 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.cpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.cpp @@ -2,10 +2,21 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include +#include +#include #include "ExtrusionLine.hpp" -#include "linearAlg2D.hpp" #include "../../VariableWidth.hpp" +#include "libslic3r/Arachne/utils/ExtrusionJunction.hpp" +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ExtrusionEntity.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/Polygon.hpp" +#include "libslic3r/Polyline.hpp" + +namespace Slic3r { +class Flow; +} // namespace Slic3r namespace Slic3r::Arachne { @@ -29,15 +40,6 @@ int64_t ExtrusionLine::getLength() const return len; } -coord_t ExtrusionLine::getMinimalWidth() const -{ - return std::min_element(junctions.cbegin(), junctions.cend(), - [](const ExtrusionJunction& l, const ExtrusionJunction& r) - { - return l.w < r.w; - })->w; -} - void ExtrusionLine::simplify(const int64_t smallest_line_segment_squared, const int64_t allowed_error_distance_squared, const int64_t maximum_extrusion_area_deviation) { const size_t min_path_size = is_closed ? 3 : 2; @@ -262,9 +264,10 @@ bool ExtrusionLine::is_contour() const return poly.is_clockwise(); } -double ExtrusionLine::area() const -{ - assert(this->is_closed); +double ExtrusionLine::area() const { + if (!this->is_closed) + return 0.; + double a = 0.; if (this->junctions.size() >= 3) { Vec2d p1 = this->junctions.back().p.cast(); @@ -274,9 +277,25 @@ 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 8438a4c0035..d8cad702ace 100644 --- a/src/libslic3r/Arachne/utils/ExtrusionLine.hpp +++ b/src/libslic3r/Arachne/utils/ExtrusionLine.hpp @@ -5,16 +5,28 @@ #ifndef UTILS_EXTRUSION_LINE_H #define UTILS_EXTRUSION_LINE_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "ExtrusionJunction.hpp" #include "../../Polyline.hpp" #include "../../Polygon.hpp" #include "../../BoundingBox.hpp" #include "../../ExtrusionEntity.hpp" #include "../../Flow.hpp" -#include "../../../clipper/clipper_z.hpp" +#include "libslic3r/Point.hpp" namespace Slic3r { class ThickPolyline; +class Flow; } namespace Slic3r::Arachne @@ -136,11 +148,6 @@ struct ExtrusionLine return ret; } - /*! - * Get the minimal width of this path - */ - coord_t getMinimalWidth() const; - /*! * Removes vertices of the ExtrusionLines to make sure that they are not too high * resolution. @@ -192,6 +199,8 @@ struct ExtrusionLine bool is_contour() const; double area() const; + + bool is_external_perimeter() const { return this->inset_idx == 0; } }; template @@ -218,6 +227,7 @@ 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); @@ -226,24 +236,11 @@ static inline Polygon to_polygon(const ExtrusionLine &line) return out; } -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; -} +Points to_points(const ExtrusionLine &extrusion_line); -#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; -} +BoundingBox get_extents(const ExtrusionLine &extrusion_line); +#if 0 static BoundingBox get_extents(const std::vector &extrusion_lines) { BoundingBox bbox; @@ -274,6 +271,8 @@ static std::vector to_points(const std::vector &e #endif using VariableWidthLines = std::vector; //; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp index 99efff6a077..17b06f2be28 100644 --- a/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp +++ b/src/libslic3r/Arachne/utils/HalfEdgeGraph.hpp @@ -21,8 +21,10 @@ class HalfEdgeGraph public: using edge_t = derived_edge_t; using node_t = derived_node_t; - std::list edges; - std::list nodes; + using Edges = std::list; + using Nodes = std::list; + Edges edges; + Nodes nodes; }; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp b/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp index 125b3ef9268..04f017f86a5 100644 --- a/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp +++ b/src/libslic3r/Arachne/utils/PolygonsPointIndex.hpp @@ -156,8 +156,6 @@ struct PathsPointIndexLocator } }; -using PolygonsPointIndexLocator = PathsPointIndexLocator; - }//namespace Slic3r::Arachne namespace std diff --git a/src/libslic3r/Arachne/utils/PolylineStitcher.cpp b/src/libslic3r/Arachne/utils/PolylineStitcher.cpp index 89ec9295402..0abf63ac104 100644 --- a/src/libslic3r/Arachne/utils/PolylineStitcher.cpp +++ b/src/libslic3r/Arachne/utils/PolylineStitcher.cpp @@ -2,7 +2,16 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include "PolylineStitcher.hpp" + #include "ExtrusionLine.hpp" +#include "libslic3r/Arachne/utils/PolygonsPointIndex.hpp" +#include "libslic3r/Polygon.hpp" + +namespace Slic3r { +namespace Arachne { +struct ExtrusionJunction; +} // namespace Arachne +} // namespace Slic3r namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/utils/PolylineStitcher.hpp b/src/libslic3r/Arachne/utils/PolylineStitcher.hpp index 2ab770a3ec0..7f547f4f23c 100644 --- a/src/libslic3r/Arachne/utils/PolylineStitcher.hpp +++ b/src/libslic3r/Arachne/utils/PolylineStitcher.hpp @@ -4,11 +4,20 @@ #ifndef UTILS_POLYLINE_STITCHER_H #define UTILS_POLYLINE_STITCHER_H +#include +#include +#include +#include +#include +#include +#include +#include + #include "SparsePointGrid.hpp" #include "PolygonsPointIndex.hpp" #include "../../Polygon.hpp" -#include -#include +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/utils/SparseGrid.hpp b/src/libslic3r/Arachne/utils/SparseGrid.hpp index be461d42412..45876fb9a7a 100644 --- a/src/libslic3r/Arachne/utils/SparseGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparseGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_GRID_H #include -#include #include #include diff --git a/src/libslic3r/Arachne/utils/SparseLineGrid.hpp b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp index a9b53686973..0b38988f92d 100644 --- a/src/libslic3r/Arachne/utils/SparseLineGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparseLineGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_LINE_GRID_H #include -#include #include #include diff --git a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp index 31c1965357b..127380b3eb9 100644 --- a/src/libslic3r/Arachne/utils/SparsePointGrid.hpp +++ b/src/libslic3r/Arachne/utils/SparsePointGrid.hpp @@ -6,7 +6,6 @@ #define UTILS_SPARSE_POINT_GRID_H #include -#include #include #include "SparseGrid.hpp" @@ -40,16 +39,6 @@ template class SparsePointGrid : public SparseGrid::GridPoint; @@ -69,22 +58,6 @@ void SparsePointGrid::insert(const Elem &elem) SparseGrid::m_grid.emplace(grid_loc, elem); } -template -const ElemT *SparsePointGrid::getAnyNearby(const Point &query_pt, coord_t radius) -{ - const ElemT *ret = nullptr; - const std::function &process_func = [&ret, query_pt, radius, this](const ElemT &maybe_nearby) { - if (shorter_then(m_locator(maybe_nearby) - query_pt, radius)) { - ret = &maybe_nearby; - return false; - } - return true; - }; - SparseGrid::processNearby(query_pt, radius, process_func); - - return ret; -} - } // namespace Slic3r::Arachne #endif // UTILS_SPARSE_POINT_GRID_H diff --git a/src/libslic3r/Arachne/utils/SquareGrid.cpp b/src/libslic3r/Arachne/utils/SquareGrid.cpp index ae899657954..1af99104262 100644 --- a/src/libslic3r/Arachne/utils/SquareGrid.cpp +++ b/src/libslic3r/Arachne/utils/SquareGrid.cpp @@ -2,7 +2,10 @@ //CuraEngine is released under the terms of the AGPLv3 or higher. #include "SquareGrid.hpp" -#include "../../Point.hpp" + +#include + +#include "libslic3r/Point.hpp" using namespace Slic3r::Arachne; diff --git a/src/libslic3r/Arachne/utils/SquareGrid.hpp b/src/libslic3r/Arachne/utils/SquareGrid.hpp index c59c3ee1b9f..ff45d853544 100644 --- a/src/libslic3r/Arachne/utils/SquareGrid.hpp +++ b/src/libslic3r/Arachne/utils/SquareGrid.hpp @@ -4,12 +4,15 @@ #ifndef UTILS_SQUARE_GRID_H #define UTILS_SQUARE_GRID_H -#include "../../Point.hpp" - +#include #include -#include #include #include +#include +#include + +#include "../../Point.hpp" +#include "libslic3r/libslic3r.h" namespace Slic3r::Arachne { diff --git a/src/libslic3r/Arachne/utils/linearAlg2D.hpp b/src/libslic3r/Arachne/utils/linearAlg2D.hpp index 1db85c268b1..7ece14c24c0 100644 --- a/src/libslic3r/Arachne/utils/linearAlg2D.hpp +++ b/src/libslic3r/Arachne/utils/linearAlg2D.hpp @@ -9,63 +9,6 @@ namespace Slic3r::Arachne::LinearAlg2D { -/*! - * Test whether a point is inside a corner. - * Whether point \p query_point is left of the corner abc. - * Whether the \p query_point is in the circle half left of ab and left of bc, rather than to the right. - * - * Test whether the \p query_point is inside of a polygon w.r.t a single corner. - */ -inline static bool isInsideCorner(const Point &a, const Point &b, const Point &c, const Vec2i64 &query_point) -{ - // Visualisation for the algorithm below: - // - // query - // | - // | - // | - // perp-----------b - // / \ (note that the lines - // / \ AB and AC are normalized - // / \ to 10000 units length) - // a c - // - - auto normal = [](const Point &p0, coord_t len) -> Point { - int64_t _len = p0.norm(); - if (_len < 1) - return {len, 0}; - return (p0.cast() * int64_t(len) / _len).cast(); - }; - - auto rotate_90_degree_ccw = [](const Vec2d &p) -> Vec2d { - return {-p.y(), p.x()}; - }; - - constexpr coord_t normal_length = 10000; //Create a normal vector of reasonable length in order to reduce rounding error. - const Point ba = normal(a - b, normal_length); - const Point bc = normal(c - b, normal_length); - const Vec2d bq = query_point.cast() - b.cast(); - const Vec2d perpendicular = rotate_90_degree_ccw(bq); //The query projects to this perpendicular to coordinate 0. - - const double project_a_perpendicular = ba.cast().dot(perpendicular); //Project vertex A on the perpendicular line. - const double project_c_perpendicular = bc.cast().dot(perpendicular); //Project vertex C on the perpendicular line. - if ((project_a_perpendicular > 0.) != (project_c_perpendicular > 0.)) //Query is between A and C on the projection. - { - return project_a_perpendicular > 0.; //Due to the winding order of corner ABC, this means that the query is inside. - } - else //Beyond either A or C, but it could still be inside of the polygon. - { - const double project_a_parallel = ba.cast().dot(bq); //Project not on the perpendicular, but on the original. - const double project_c_parallel = bc.cast().dot(bq); - - //Either: - // * A is to the right of B (project_a_perpendicular > 0) and C is below A (project_c_parallel < project_a_parallel), or - // * A is to the left of B (project_a_perpendicular < 0) and C is above A (project_c_parallel > project_a_parallel). - return (project_c_parallel < project_a_parallel) == (project_a_perpendicular > 0.); - } -} - /*! * Returns the determinant of the 2D matrix defined by the the vectors ab and ap as rows. * diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 2c362dba23e..da4fb9c1df9 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -420,7 +420,6 @@ set(lisbslic3r_sources Arachne/BeadingStrategy/WideningBeadingStrategy.hpp Arachne/BeadingStrategy/WideningBeadingStrategy.cpp Arachne/utils/ExtrusionJunction.hpp - Arachne/utils/ExtrusionJunction.cpp Arachne/utils/ExtrusionLine.hpp Arachne/utils/ExtrusionLine.cpp Arachne/utils/HalfEdge.hpp @@ -435,6 +434,8 @@ 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/Geometry.cpp b/src/libslic3r/Geometry.cpp index 3e67d4e1ba4..7c89a5c00ae 100644 --- a/src/libslic3r/Geometry.cpp +++ b/src/libslic3r/Geometry.cpp @@ -839,4 +839,50 @@ TransformationSVD::TransformationSVD(const Transform3d& trafo) return curMat; } +bool is_point_inside_polygon_corner(const Point &a, const Point &b, const Point &c, const Point &query_point) { + // Cast all input points into int64_t to prevent overflows when points are close to max values of coord_t. + const Vec2i64 a_i64 = a.cast(); + const Vec2i64 b_i64 = b.cast(); + const Vec2i64 c_i64 = c.cast(); + const Vec2i64 query_point_i64 = query_point.cast(); + + // Shift all points to have a base in vertex B. + // Then construct normalized vectors to ensure that we will work with vectors with endpoints on the unit circle. + const Vec2d ba = (a_i64 - b_i64).cast().normalized(); + const Vec2d bc = (c_i64 - b_i64).cast().normalized(); + const Vec2d bq = (query_point_i64 - b_i64).cast().normalized(); + + // Points A and C has to be different. + assert(ba != bc); + + // Construct a normal for the vector BQ that points to the left side of the vector BQ. + const Vec2d bq_left_normal = perp(bq); + + const double proj_a_on_bq_normal = ba.dot(bq_left_normal); // Project point A on the normal of BQ. + const double proj_c_on_bq_normal = bc.dot(bq_left_normal); // Project point C on the normal of BQ. + if ((proj_a_on_bq_normal > 0. && proj_c_on_bq_normal <= 0.) || (proj_a_on_bq_normal <= 0. && proj_c_on_bq_normal > 0.)) { + // Q is between points A and C or lies on one of those vectors (BA or BC). + + // Based on the CCW order of polygons (contours) and order of corner ABC, + // when this condition is met, the query point is inside the corner. + return proj_a_on_bq_normal > 0.; + } else { + // Q isn't between points A and C, but still it can be inside the corner. + + const double proj_a_on_bq = ba.dot(bq); // Project point A on BQ. + const double proj_c_on_bq = bc.dot(bq); // Project point C on BQ. + + // The value of proj_a_on_bq_normal is the same when we project the vector BA on the normal of BQ. + // So we can say that the Q is on the right side of the vector BA when proj_a_on_bq_normal > 0, and + // that the Q is on the left side of the vector BA proj_a_on_bq_normal < 0. + // Also, the Q is on the right side of the bisector of oriented angle ABC when proj_c_on_bq < proj_a_on_bq, and + // the Q is on the left side of the bisector of oriented angle ABC when proj_c_on_bq > proj_a_on_bq. + + // So the Q is inside the corner when one of the following conditions is met: + // * The Q is on the right side of the vector BA, and the Q is on the right side of the bisector of the oriented angle ABC. + // * The Q is on the left side of the vector BA, and the Q is on the left side of the bisector of the oriented angle ABC. + return (proj_a_on_bq_normal > 0. && proj_c_on_bq < proj_a_on_bq) || (proj_a_on_bq_normal <= 0. && proj_c_on_bq >= proj_a_on_bq); + } +} + }} // namespace Slic3r::Geometry diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 8183dcb7e81..0383aadb07e 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -545,6 +545,23 @@ inline bool is_rotation_ninety_degrees(const Vec3d &rotation) } Transformation mat_around_a_point_rotate(const Transformation& innMat, const Vec3d &pt, const Vec3d &axis, float rotate_theta_radian); + +/** + * Checks if a given point is inside a corner of a polygon. + * + * The corner of a polygon is defined by three points A, B, C in counterclockwise order. + * + * Adapted from CuraEngine LinearAlg2D::isInsideCorner by Tim Kuipers @BagelOrb + * and @Ghostkeeper. + * + * @param a The first point of the corner. + * @param b The second point of the corner (the common vertex of the two edges forming the corner). + * @param c The third point of the corner. + * @param query_point The point to be checked if is inside the corner. + * @return True if the query point is inside the corner, false otherwise. + */ +bool is_point_inside_polygon_corner(const Point &a, const Point &b, const Point &c, const Point &query_point); + } } // namespace Slicer::Geometry #endif diff --git a/src/libslic3r/Geometry/MedialAxis.cpp b/src/libslic3r/Geometry/MedialAxis.cpp index f3514bb5125..7fece75e633 100644 --- a/src/libslic3r/Geometry/MedialAxis.cpp +++ b/src/libslic3r/Geometry/MedialAxis.cpp @@ -1,9 +1,16 @@ #include #include "MedialAxis.hpp" -#include "clipper.hpp" +#include +#include +#include +#include + #include "VoronoiOffset.hpp" -#include "../ClipperUtils.hpp" +#include "libslic3r/ClipperUtils.hpp" +#include "libslic3r/ExPolygon.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" #ifdef SLIC3R_DEBUG namespace boost { namespace polygon { diff --git a/src/libslic3r/Geometry/MedialAxis.hpp b/src/libslic3r/Geometry/MedialAxis.hpp index b1354ddb2f2..cd1404f915b 100644 --- a/src/libslic3r/Geometry/MedialAxis.hpp +++ b/src/libslic3r/Geometry/MedialAxis.hpp @@ -1,8 +1,15 @@ #ifndef slic3r_Geometry_MedialAxis_hpp_ #define slic3r_Geometry_MedialAxis_hpp_ +#include +#include +#include +#include + #include "Voronoi.hpp" #include "../ExPolygon.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/Polyline.hpp" namespace Slic3r::Geometry { diff --git a/src/libslic3r/Geometry/Voronoi.cpp b/src/libslic3r/Geometry/Voronoi.cpp index 58923b9ada6..4f7173e5c28 100644 --- a/src/libslic3r/Geometry/Voronoi.cpp +++ b/src/libslic3r/Geometry/Voronoi.cpp @@ -1,11 +1,13 @@ #include "Voronoi.hpp" +#include +#include + #include "libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp" #include "libslic3r/Geometry/VoronoiUtils.hpp" #include "libslic3r/Geometry/VoronoiUtilsCgal.hpp" #include "libslic3r/MultiMaterialSegmentation.hpp" - -#include +#include "libslic3r/Line.hpp" namespace Slic3r::Geometry { @@ -35,6 +37,8 @@ VoronoiDiagram::construct_voronoi(const SegmentIterator segment_begin, const Seg BOOST_LOG_TRIVIAL(warning) << "Detected Voronoi edge intersecting input segment, input polygons will be rotated back and forth."; } else if (m_issue_type == IssueType::FINITE_EDGE_WITH_NON_FINITE_VERTEX) { BOOST_LOG_TRIVIAL(warning) << "Detected finite Voronoi vertex with non finite vertex, input polygons will be rotated back and forth."; + } else if (m_issue_type == IssueType::PARABOLIC_VORONOI_EDGE_WITHOUT_FOCUS_POINT) { + BOOST_LOG_TRIVIAL(warning) << "Detected parabolic Voronoi edges without focus point, input polygons will be rotated back and forth."; } else { BOOST_LOG_TRIVIAL(error) << "Detected unknown Voronoi diagram issue, input polygons will be rotated back and forth."; } @@ -48,6 +52,8 @@ VoronoiDiagram::construct_voronoi(const SegmentIterator segment_begin, const Seg BOOST_LOG_TRIVIAL(error) << "Detected Voronoi edge intersecting input segment even after the rotation of input."; } else if (m_issue_type == IssueType::FINITE_EDGE_WITH_NON_FINITE_VERTEX) { BOOST_LOG_TRIVIAL(error) << "Detected finite Voronoi vertex with non finite vertex even after the rotation of input."; + } else if (m_issue_type == IssueType::PARABOLIC_VORONOI_EDGE_WITHOUT_FOCUS_POINT) { + BOOST_LOG_TRIVIAL(error) << "Detected parabolic Voronoi edges without focus point even after the rotation of input."; } else { BOOST_LOG_TRIVIAL(error) << "Detected unknown Voronoi diagram issue even after the rotation of input."; } @@ -159,16 +165,14 @@ typename boost::polygon::enable_if< VoronoiDiagram::IssueType>::type VoronoiDiagram::detect_known_issues(const VoronoiDiagram &voronoi_diagram, SegmentIterator segment_begin, SegmentIterator segment_end) { - if (has_finite_edge_with_non_finite_vertex(voronoi_diagram)) { - return IssueType::FINITE_EDGE_WITH_NON_FINITE_VERTEX; + if (const IssueType edge_issue_type = detect_known_voronoi_edge_issues(voronoi_diagram); edge_issue_type != IssueType::NO_ISSUE_DETECTED) { + return edge_issue_type; } else if (const IssueType cell_issue_type = detect_known_voronoi_cell_issues(voronoi_diagram, segment_begin, segment_end); cell_issue_type != IssueType::NO_ISSUE_DETECTED) { return cell_issue_type; + } else if (!VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segment_begin, segment_end)) { + // Detection of non-planar Voronoi diagram detects at least GH issues #8474, #8514 and #8446. + return IssueType::NON_PLANAR_VORONOI_DIAGRAM; } - // BBS: test no problem in BBS - //} else if (!VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segment_begin, segment_end)) { - // // Detection of non-planar Voronoi diagram detects at least GH issues #8474, #8514 and #8446. - // return IssueType::NON_PLANAR_VORONOI_DIAGRAM; - //} return IssueType::NO_ISSUE_DETECTED; } @@ -220,16 +224,20 @@ VoronoiDiagram::detect_known_voronoi_cell_issues(const VoronoiDiagram &voronoi_d return IssueType::NO_ISSUE_DETECTED; } -bool VoronoiDiagram::has_finite_edge_with_non_finite_vertex(const VoronoiDiagram &voronoi_diagram) +VoronoiDiagram::IssueType VoronoiDiagram::detect_known_voronoi_edge_issues(const VoronoiDiagram &voronoi_diagram) { for (const voronoi_diagram_type::edge_type &edge : voronoi_diagram.edges()) { if (edge.is_finite()) { assert(edge.vertex0() != nullptr && edge.vertex1() != nullptr); if (edge.vertex0() == nullptr || edge.vertex1() == nullptr || !VoronoiUtils::is_finite(*edge.vertex0()) || !VoronoiUtils::is_finite(*edge.vertex1())) - return true; + return IssueType::FINITE_EDGE_WITH_NON_FINITE_VERTEX; + + if (edge.is_curved() && !edge.cell()->contains_point() && !edge.twin()->cell()->contains_point()) + return IssueType::PARABOLIC_VORONOI_EDGE_WITHOUT_FOCUS_POINT; } } - return false; + + return IssueType::NO_ISSUE_DETECTED; } template diff --git a/src/libslic3r/Geometry/Voronoi.hpp b/src/libslic3r/Geometry/Voronoi.hpp index ef1c622769f..23a50bb8f74 100644 --- a/src/libslic3r/Geometry/Voronoi.hpp +++ b/src/libslic3r/Geometry/Voronoi.hpp @@ -1,8 +1,15 @@ #ifndef slic3r_Geometry_Voronoi_hpp_ #define slic3r_Geometry_Voronoi_hpp_ +#include +#include +#include +#include + #include "../Line.hpp" #include "../Polyline.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" #ifdef _MSC_VER // Suppress warning C4146 in OpenVDB: unary minus operator applied to unsigned type, result still unsigned @@ -10,6 +17,12 @@ #pragma warning(disable : 4146) #endif // _MSC_VER #include "boost/polygon/voronoi.hpp" + +namespace boost { +namespace polygon { +template struct segment_traits; +} // namespace polygon +} // namespace boost #ifdef _MSC_VER #pragma warning(pop) #endif // _MSC_VER @@ -44,7 +57,8 @@ class VoronoiDiagram MISSING_VORONOI_VERTEX, NON_PLANAR_VORONOI_DIAGRAM, VORONOI_EDGE_INTERSECTING_INPUT_SEGMENT, - UNKNOWN // Repairs are disabled in the constructor. + PARABOLIC_VORONOI_EDGE_WITHOUT_FOCUS_POINT, + UNKNOWN // Repairs are disabled in the constructor. }; enum class State { @@ -158,7 +172,10 @@ class VoronoiDiagram IssueType>::type detect_known_voronoi_cell_issues(const VoronoiDiagram &voronoi_diagram, SegmentIterator segment_begin, SegmentIterator segment_end); - static bool has_finite_edge_with_non_finite_vertex(const VoronoiDiagram &voronoi_diagram); + // Detect issues related to Voronoi edges, or that can be detected by iterating over Voronoi edges. + // The first type of issue that can be detected is a finite Voronoi edge with a non-finite vertex. + // The second type of issue that can be detected is a parabolic Voronoi edge without a focus point (produced by two segments). + static IssueType detect_known_voronoi_edge_issues(const VoronoiDiagram &voronoi_diagram); voronoi_diagram_type m_voronoi_diagram; vertex_container_type m_vertices; diff --git a/src/libslic3r/Geometry/VoronoiOffset.cpp b/src/libslic3r/Geometry/VoronoiOffset.cpp index 46105220a4b..8ecd370235c 100644 --- a/src/libslic3r/Geometry/VoronoiOffset.cpp +++ b/src/libslic3r/Geometry/VoronoiOffset.cpp @@ -1,15 +1,21 @@ // Polygon offsetting using Voronoi diagram prodiced by boost::polygon. -#include "Geometry.hpp" -#include "VoronoiOffset.hpp" -#include "libslic3r.h" - #include +#include +#include +#include +#include +#include +#include +#include + +#include "libslic3r/Geometry.hpp" +#include "VoronoiOffset.hpp" +#include "libslic3r/libslic3r.h" +#include "libslic3r/Geometry/Voronoi.hpp" // #define VORONOI_DEBUG_OUT -#include - #ifdef VORONOI_DEBUG_OUT #include #endif diff --git a/src/libslic3r/Geometry/VoronoiOffset.hpp b/src/libslic3r/Geometry/VoronoiOffset.hpp index 359fe010c85..747538e8b69 100644 --- a/src/libslic3r/Geometry/VoronoiOffset.hpp +++ b/src/libslic3r/Geometry/VoronoiOffset.hpp @@ -3,9 +3,15 @@ #ifndef slic3r_VoronoiOffset_hpp_ #define slic3r_VoronoiOffset_hpp_ -#include "../libslic3r.h" +#include +#include +#include +#include "libslic3r/libslic3r.h" #include "Voronoi.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/Polygon.hpp" namespace Slic3r { diff --git a/src/libslic3r/Geometry/VoronoiUtils.cpp b/src/libslic3r/Geometry/VoronoiUtils.cpp index f126258d280..f140782891c 100644 --- a/src/libslic3r/Geometry/VoronoiUtils.cpp +++ b/src/libslic3r/Geometry/VoronoiUtils.cpp @@ -1,10 +1,17 @@ #include - -#include -#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "VoronoiUtils.hpp" -#include "libslic3r.h" +#include "libslic3r/Exception.hpp" +#include "libslic3r/Line.hpp" namespace Slic3r::Geometry { @@ -28,6 +35,7 @@ template SegmentCellRange VoronoiUtils::compute_segment_cell_range(const template SegmentCellRange VoronoiUtils::compute_segment_cell_range(const VoronoiDiagram::cell_type &, VD::SegmentIt, VD::SegmentIt); template SegmentCellRange VoronoiUtils::compute_segment_cell_range(const VoronoiDiagram::cell_type &, ColoredLinesConstIt, ColoredLinesConstIt); template SegmentCellRange VoronoiUtils::compute_segment_cell_range(const VoronoiDiagram::cell_type &, PolygonsSegmentIndexConstIt, PolygonsSegmentIndexConstIt); +template PointCellRange VoronoiUtils::compute_point_cell_range(const VoronoiDiagram::cell_type &, PolygonsSegmentIndexConstIt, PolygonsSegmentIndexConstIt); template Points VoronoiUtils::discretize_parabola(const Point &, const Arachne::PolygonsSegmentIndex &, const Point &, const Point &, coord_t, float); template Arachne::PolygonsPointIndex VoronoiUtils::get_source_point_index(const VoronoiDiagram::cell_type &, PolygonsSegmentIndexConstIt, PolygonsSegmentIndexConstIt); @@ -124,7 +132,7 @@ VoronoiUtils::discretize_parabola(const Point &source_point, const Segment &sour Point pxx; Line(a, b).distance_to_infinite_squared(source_point, &pxx); const Point ppxx = pxx - source_point; - const coord_t d = ppxx.norm(); + const coord_t d = ppxx.cast().norm(); const Vec2d rot = perp(ppxx).cast().normalized(); const double rot_cos_theta = rot.x(); @@ -137,8 +145,8 @@ VoronoiUtils::discretize_parabola(const Point &source_point, const Segment &sour } const double marking_bound = atan(transitioning_angle * 0.5); - int64_t msx = -marking_bound * d; // projected marking_start - int64_t mex = marking_bound * d; // projected marking_end + int64_t msx = -marking_bound * int64_t(d); // projected marking_start + int64_t mex = marking_bound * int64_t(d); // projected marking_end const coord_t marking_start_end_h = msx * msx / (2 * d) + d / 2; Point marking_start = Point(coord_t(msx), marking_start_end_h).rotated(rot_cos_theta, rot_sin_theta) + pxx; @@ -152,7 +160,7 @@ VoronoiUtils::discretize_parabola(const Point &source_point, const Segment &sour bool add_marking_start = msx * int64_t(dir) > int64_t(sx - px) * int64_t(dir) && msx * int64_t(dir) < int64_t(ex - px) * int64_t(dir); bool add_marking_end = mex * int64_t(dir) > int64_t(sx - px) * int64_t(dir) && mex * int64_t(dir) < int64_t(ex - px) * int64_t(dir); - const Point apex = Point(coord_t(0), coord_t(d / 2)).rotated(rot_cos_theta, rot_sin_theta) + pxx; + const Point apex = Point(0, d / 2).rotated(rot_cos_theta, rot_sin_theta) + pxx; bool add_apex = int64_t(sx - px) * int64_t(dir) < 0 && int64_t(ex - px) * int64_t(dir) > 0; assert(!add_marking_start || !add_marking_end || add_apex); @@ -245,6 +253,62 @@ VoronoiUtils::compute_segment_cell_range(const VD::cell_type &cell, const Segmen return cell_range; } +template +typename boost::polygon::enable_if< + typename boost::polygon::gtl_if::value_type>::type>::type>::type, + Geometry::PointCellRange< + typename boost::polygon::segment_point_type::value_type>::type>>::type +VoronoiUtils::compute_point_cell_range(const VD::cell_type &cell, const SegmentIterator segment_begin, const SegmentIterator segment_end) +{ + using Segment = typename std::iterator_traits::value_type; + using Point = typename boost::polygon::segment_point_type::type; + using PointCellRange = PointCellRange; + using CoordType = typename Point::coord_type; + + const Point source_point = Geometry::VoronoiUtils::get_source_point(cell, segment_begin, segment_end); + + // We want to ignore (by returning PointCellRange without assigned edge_begin and edge_end) cells outside the input polygon. + PointCellRange cell_range(source_point); + + const VD::edge_type *edge = cell.incident_edge(); + if (edge->is_infinite() || !is_in_range(*edge)) { + // Ignore infinite edges, because they only occur outside the polygon. + // Also ignore edges with endpoints that don't fit into CoordType, because such edges are definitely outside the polygon. + return cell_range; + } + + const Arachne::PolygonsPointIndex source_point_idx = Geometry::VoronoiUtils::get_source_point_index(cell, segment_begin, segment_end); + const Point edge_v0 = Geometry::VoronoiUtils::to_point(edge->vertex0()).template cast(); + const Point edge_v1 = Geometry::VoronoiUtils::to_point(edge->vertex1()).template cast(); + const Point edge_query_point = (edge_v0 == source_point) ? edge_v1 : edge_v0; + + // Check if the edge has another endpoint inside the corner of the polygon. + if (!Geometry::is_point_inside_polygon_corner(source_point_idx.prev().p(), source_point_idx.p(), source_point_idx.next().p(), edge_query_point)) { + // If the endpoint isn't inside the corner of the polygon, it means that + // the whole cell isn't inside the polygons, and we will ignore such cells. + return cell_range; + } + + const Vec2i64 source_point_i64 = source_point.template cast(); + edge = cell.incident_edge(); + do { + assert(edge->is_finite()); + + if (Vec2i64 v1 = Geometry::VoronoiUtils::to_point(edge->vertex1()); v1 == source_point_i64) { + cell_range.edge_begin = edge->next(); + cell_range.edge_end = edge; + } else { + // FIXME @hejllukas: With Arachne, we don't support polygons with collinear edges, + // because with collinear edges we have to handle secondary edges. + // Such edges goes through the endpoints of the input segments. + assert((Geometry::VoronoiUtils::to_point(edge->vertex0()) == source_point_i64 || edge->is_primary()) && "Point cells must end in the point! They cannot cross the point with an edge, because collinear edges are not allowed in the input."); + } + } while (edge = edge->next(), edge != cell.incident_edge()); + + return cell_range; +} + Vec2i64 VoronoiUtils::to_point(const VD::vertex_type *vertex) { assert(vertex != nullptr); diff --git a/src/libslic3r/Geometry/VoronoiUtils.hpp b/src/libslic3r/Geometry/VoronoiUtils.hpp index bf63914677b..6872e4ea721 100644 --- a/src/libslic3r/Geometry/VoronoiUtils.hpp +++ b/src/libslic3r/Geometry/VoronoiUtils.hpp @@ -1,8 +1,15 @@ #ifndef slic3r_VoronoiUtils_hpp_ #define slic3r_VoronoiUtils_hpp_ +#include +#include +#include + #include "libslic3r/Geometry/Voronoi.hpp" #include "libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp" +#include "libslic3r/Arachne/utils/PolygonsPointIndex.hpp" +#include "libslic3r/Point.hpp" +#include "libslic3r/libslic3r.h" using VD = Slic3r::Geometry::VoronoiDiagram; @@ -11,19 +18,32 @@ namespace Slic3r::Geometry { // Represent trapezoid Voronoi cell around segment. template struct SegmentCellRange { - const PT segment_start_point; // The start point of the source segment of this cell. - const PT segment_end_point; // The end point of the source segment of this cell. - const VD::edge_type *edge_begin = nullptr; // The edge of the Voronoi diagram where the loop around the cell starts. - const VD::edge_type *edge_end = nullptr; // The edge of the Voronoi diagram where the loop around the cell ends. + const PT source_segment_start_point; // The start point of the source segment of this cell. + const PT source_segment_end_point; // The end point of the source segment of this cell. + const VD::edge_type *edge_begin = nullptr; // The edge of the Voronoi diagram where the loop around the cell starts. + const VD::edge_type *edge_end = nullptr; // The edge of the Voronoi diagram where the loop around the cell ends. SegmentCellRange() = delete; - explicit SegmentCellRange(const PT &segment_start_point, const PT &segment_end_point) - : segment_start_point(segment_start_point), segment_end_point(segment_end_point) + explicit SegmentCellRange(const PT &source_segment_start_point, const PT &source_segment_end_point) + : source_segment_start_point(source_segment_start_point), source_segment_end_point(source_segment_end_point) {} bool is_valid() const { return edge_begin && edge_end && edge_begin != edge_end; } }; +// Represent trapezoid Voronoi cell around point. +template struct PointCellRange +{ + const PT source_point; // The source point of this cell. + const VD::edge_type *edge_begin = nullptr; // The edge of the Voronoi diagram where the loop around the cell starts. + const VD::edge_type *edge_end = nullptr; // The edge of the Voronoi diagram where the loop around the cell ends. + + PointCellRange() = delete; + explicit PointCellRange(const PT &source_point) : source_point(source_point) {} + + bool is_valid() const { return edge_begin && edge_end && edge_begin != edge_end; } +}; + class VoronoiUtils { public: @@ -80,7 +100,7 @@ class VoronoiUtils * are linked to the neighboring segments, so you can iterate over the * segments until you reach the last segment. * - * Adapted from CuraEngine VoronoiUtils::computePointCellRange by Tim Kuipers @BagelOrb, + * Adapted from CuraEngine VoronoiUtils::computeSegmentCellRange by Tim Kuipers @BagelOrb, * Jaime van Kessel @nallath, Remco Burema @rburema and @Ghostkeeper. * * @param cell The cell to compute the range of line segments for. @@ -96,6 +116,33 @@ class VoronoiUtils typename boost::polygon::segment_point_type::value_type>::type>>::type compute_segment_cell_range(const VD::cell_type &cell, SegmentIterator segment_begin, SegmentIterator segment_end); + /** + * Compute the range of line segments that surround a cell of the skeletal + * graph that belongs to a point on the medial axis. + * + * This should only be used on cells that belong to a corner in the skeletal + * graph, e.g. triangular cells, not trapezoid cells. + * + * The resulting line segments is just the first and the last segment. They + * are linked to the neighboring segments, so you can iterate over the + * segments until you reach the last segment. + * + * Adapted from CuraEngine VoronoiUtils::computePointCellRange by Tim Kuipers @BagelOrb + * Jaime van Kessel @nallath, Remco Burema @rburema and @Ghostkeeper. + * + * @param cell The cell to compute the range of line segments for. + * @param segment_begin Begin iterator for all edges of the input Polygons. + * @param segment_end End iterator for all edges of the input Polygons. + * @return Range of line segments that surround the cell. + */ + template + static typename boost::polygon::enable_if< + typename boost::polygon::gtl_if::value_type>::type>::type>::type, + Geometry::PointCellRange< + typename boost::polygon::segment_point_type::value_type>::type>>::type + compute_point_cell_range(const VD::cell_type &cell, SegmentIterator segment_begin, SegmentIterator segment_end); + template static bool is_in_range(double value) { return double(std::numeric_limits::lowest()) <= value && value <= double(std::numeric_limits::max()); diff --git a/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp index 3118bf82803..7797a5a29fd 100644 --- a/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp @@ -1,15 +1,21 @@ -// Needed since the CGAL headers are not self-contained. -#include #include #include #include +#include +#include +#include #include "libslic3r/Geometry/Voronoi.hpp" #include "libslic3r/Geometry/VoronoiUtils.hpp" #include "libslic3r/Arachne/utils/PolygonsSegmentIndex.hpp" #include "libslic3r/MultiMaterialSegmentation.hpp" - #include "VoronoiUtilsCgal.hpp" +#include "libslic3r/Line.hpp" +#include "libslic3r/Point.hpp" + +namespace CGAL { +class MP_Float; +} // namespace CGAL using VD = Slic3r::Geometry::VoronoiDiagram; diff --git a/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp b/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp index 33ae847802c..baafbc7a1a7 100644 --- a/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp @@ -1,6 +1,9 @@ #ifndef slic3r_VoronoiUtilsCgal_hpp_ #define slic3r_VoronoiUtilsCgal_hpp_ +#include +#include + #include "Voronoi.hpp" #include "../Arachne/utils/PolygonsSegmentIndex.hpp" diff --git a/src/libslic3r/Geometry/VoronoiVisualUtils.hpp b/src/libslic3r/Geometry/VoronoiVisualUtils.hpp index 177f27293a2..c89d59bf9a0 100644 --- a/src/libslic3r/Geometry/VoronoiVisualUtils.hpp +++ b/src/libslic3r/Geometry/VoronoiVisualUtils.hpp @@ -1,3 +1,6 @@ +#ifndef slic3r_VoronoiVisualUtils_hpp_ +#define slic3r_VoronoiVisualUtils_hpp_ + #include #include @@ -451,3 +454,5 @@ static inline void dump_voronoi_to_svg( } } // namespace Slic3r + +#endif // slic3r_VoronoiVisualUtils_hpp_ diff --git a/src/libslic3r/PerimeterGenerator.cpp b/src/libslic3r/PerimeterGenerator.cpp index 624a710902b..5c142db4715 100644 --- a/src/libslic3r/PerimeterGenerator.cpp +++ b/src/libslic3r/PerimeterGenerator.cpp @@ -9,6 +9,7 @@ #include "VariableWidth.hpp" #include "CurveAnalyzer.hpp" #include "Clipper2Utils.hpp" +#include "Arachne/PerimeterOrder.hpp" #include "Arachne/WallToolPaths.hpp" #include "Geometry/ConvexHull.hpp" #include "ExPolygonCollection.hpp" @@ -566,7 +567,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); @@ -876,13 +877,6 @@ 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) { @@ -967,7 +961,7 @@ static void smooth_overhang_level(ExtrusionPaths &paths) } } -static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& perimeter_generator, std::vector& pg_extrusions, +static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& perimeter_generator, Arachne::PerimeterOrder::PerimeterExtrusions& pg_extrusions, bool &steep_overhang_contour, bool &steep_overhang_hole) { // Detect steep overhangs @@ -975,34 +969,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 (PerimeterGeneratorArachneExtrusion& pg_extrusion : pg_extrusions) { - Arachne::ExtrusionLine* extrusion = pg_extrusion.extrusion; - if (extrusion->empty()) + for (Arachne::PerimeterOrder::PerimeterExtrusion &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; @@ -1011,19 +1005,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(); }; @@ -1040,7 +1034,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(); @@ -1059,9 +1053,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())); } @@ -1091,7 +1085,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; @@ -1099,7 +1093,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) { @@ -1117,7 +1111,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; } @@ -1185,7 +1179,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. @@ -1225,7 +1219,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; @@ -1242,13 +1236,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? @@ -1883,32 +1877,186 @@ static void group_region_by_fuzzify(PerimeterGenerator& g) } } -void PerimeterGenerator::process_classic() + +// 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_width = this->perimeter_flow.scaled_width(); - coord_t perimeter_spacing = this->perimeter_flow.scaled_spacing(); + 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; - // Orca: ignore precise_outer_wall if wall_sequence is not InnerOuter - if(config->precise_outer_wall && this->config->wall_sequence == WallSequence::InnerOuter) - ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.width() + this->perimeter_flow.width())); - else - ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.spacing() + this->perimeter_flow.spacing())); - + 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(); - + m_mm3_per_mm_overhang = this->overhang_flow.mm3_per_mm(); + // solid infill - coord_t solid_infill_spacing = this->solid_infill_flow.scaled_spacing(); + 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) { @@ -1918,501 +2066,308 @@ void PerimeterGenerator::process_classic() 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))); } - - // Calculate the minimum required spacing between two adjacent traces. - // This should be equal to the nominal flow spacing but we experiment - // with some tolerance in order to avoid triggering medial axis when - // some squishing might work. Loops are still spaced by the entire - // flow spacing; this only applies to collapsing parts. - // For ext_min_spacing we use the ext_perimeter_spacing calculated for two adjacent - // external loops (which is the correct way) instead of using ext_perimeter_spacing2 - // which is the spacing between external and internal, which is not correct - // and would make the collapsing (thus the details resolution) dependent on - // internal flow which is unrelated. - coord_t min_spacing = coord_t(perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE)); - coord_t ext_min_spacing = coord_t(ext_perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE)); - bool has_gap_fill = this->config->gap_infill_speed.value > 0; - // BBS: this flow is for smaller external perimeter for small area - coord_t ext_min_spacing_smaller = coord_t(ext_perimeter_spacing * (1 - SMALLER_EXT_INSET_OVERLAP_TOLERANCE)); - this->smaller_ext_perimeter_flow = this->ext_perimeter_flow; - // BBS: to be checked - this->smaller_ext_perimeter_flow = this->smaller_ext_perimeter_flow.with_width(SCALING_FACTOR * - (ext_perimeter_width - 0.5 * SMALLER_EXT_INSET_OVERLAP_TOLERANCE * ext_perimeter_spacing)); - m_ext_mm3_per_mm_smaller_width = this->smaller_ext_perimeter_flow.mm3_per_mm(); - - // prepare grown lower layer slices for overhang detection - m_lower_polygons_series = generate_lower_polygons_series(this->perimeter_flow.width()); - m_lower_overhang_dist_boundary = dist_boundary(this->perimeter_flow.width()); - if (ext_perimeter_width == perimeter_width){ - m_external_lower_polygons_series = m_lower_polygons_series; - m_external_overhang_dist_boundary=m_lower_overhang_dist_boundary; - } else { - m_external_lower_polygons_series = generate_lower_polygons_series(this->ext_perimeter_flow.width()); - m_external_overhang_dist_boundary = dist_boundary(this->ext_perimeter_flow.width()); - } - m_smaller_external_lower_polygons_series = generate_lower_polygons_series(this->smaller_ext_perimeter_flow.width()); - m_smaller_external_overhang_dist_boundary = dist_boundary(this->smaller_ext_perimeter_flow.width()); - // we need to process each island separately because we might have different - // extra perimeters for each one 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; - //BBS: reorder the surface to reduce the travel time - ExPolygons surface_exp; - for (const Surface &surface : all_surfaces) - surface_exp.push_back(surface.expolygon); - std::vector surface_order = chain_expolygons(surface_exp); - for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) { - const Surface &surface = all_surfaces[surface_order[order_idx]]; + // 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 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++; - if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer) + + // 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; - // Set the topmost layer to be one wall - if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr) + + // 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; - ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution)); - ExPolygons gaps; - ExPolygons top_fills; - ExPolygons fill_clip; - if (loop_number >= 0) { - // In case no perimeters are to be generated, loop_number will equal to -1. - std::vector contours(loop_number+1); // depth => loops - std::vector holes(loop_number+1); // depth => loops - ThickPolylines thin_walls; - // we loop one time more than needed in order to find gaps after the last perimeter was applied - for (int i = 0;; ++ i) { // outer loop is 0 - // Calculate next onion shell of perimeters. - ExPolygons offsets; - ExPolygons offsets_with_smaller_width; - if (i == 0) { - // look for thin walls - if (this->config->detect_thin_wall) { - // the minimum thickness of a single loop is: - // ext_width/2 + ext_spacing/2 + spacing/2 + width/2 - offsets = offset2_ex(last, - -float(ext_perimeter_width / 2. + ext_min_spacing / 2. - 1), - +float(ext_min_spacing / 2. - 1)); - // the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width - // (actually, something larger than that still may exist due to mitering or other causes) - coord_t min_width = coord_t(scale_(this->ext_perimeter_flow.nozzle_diameter() / 3)); - ExPolygons expp = opening_ex( - // medial axis requires non-overlapping geometry - diff_ex(last, offset(offsets, float(ext_perimeter_width / 2.) + ClipperSafetyOffset)), - float(min_width / 2.)); - // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop - for (ExPolygon &ex : expp) - ex.medial_axis(min_width, ext_perimeter_width + ext_perimeter_spacing2, &thin_walls); - } else { - coord_t ext_perimeter_smaller_width = this->smaller_ext_perimeter_flow.scaled_width(); - for (const ExPolygon& expolygon : last) { - // BBS: judge whether it's narrow but not too long island which is hard to place two line - ExPolygons expolys; - expolys.push_back(expolygon); - ExPolygons offset_result = offset2_ex(expolys, - -float(ext_perimeter_width / 2. + ext_min_spacing_smaller / 2.), - +float(ext_min_spacing_smaller / 2.)); - if (offset_result.empty() && - expolygon.area() < (double)(ext_perimeter_width + ext_min_spacing_smaller) * scale_(narrow_loop_length_threshold)) { - // BBS: for narrow external loop, use smaller line width - ExPolygons temp_result = offset_ex(expolygon, -float(ext_perimeter_smaller_width / 2.)); - offsets_with_smaller_width.insert(offsets_with_smaller_width.end(), temp_result.begin(), temp_result.end()); - } - else { - //BBS: for not narrow loop, use normal external perimeter line width - ExPolygons temp_result = offset_ex(expolygon, -float(ext_perimeter_width / 2.)); - offsets.insert(offsets.end(), temp_result.begin(), temp_result.end()); - } - } - } - if (m_spiral_vase && (offsets.size() > 1 || offsets_with_smaller_width.size() > 1)) { - // Remove all but the largest area polygon. - keep_largest_contour_only(offsets); - //BBS - if (offsets.empty()) - //BBS: only have small width loop, then keep the largest in spiral vase mode - keep_largest_contour_only(offsets_with_smaller_width); - else - //BBS: have large area, clean the small width loop - offsets_with_smaller_width.clear(); - } - } else { - //FIXME Is this offset correct if the line width of the inner perimeters differs - // from the line width of the infill? - coord_t distance = (i == 1) ? ext_perimeter_spacing2 : perimeter_spacing; - //BBS - //offsets = this->config->thin_walls ? - // This path will ensure, that the perimeters do not overfill, as in - // prusa3d/Slic3r GH #32, but with the cost of rounding the perimeters - // excessively, creating gaps, which then need to be filled in by the not very - // reliable gap fill algorithm. - // Also the offset2(perimeter, -x, x) may sometimes lead to a perimeter, which is larger than - // the original. - //offset2_ex(last, - // - float(distance + min_spacing / 2. - 1.), - // float(min_spacing / 2. - 1.)) : - // If "detect thin walls" is not enabled, this paths will be entered, which - // leads to overflows, as in prusa3d/Slic3r GH #32 - //offset_ex(last, - float(distance)); + coord_t wall_0_inset = 0; + if (apply_precise_outer_wall) + wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); - //BBS: For internal perimeter, we should "enable" thin wall strategy in which offset2 is used to - // remove too closed line, so that gap fill can be used for such internal narrow area in following - // handling. - offsets = offset2_ex(last, - -float(distance + min_spacing / 2. - 1.), - float(min_spacing / 2. - 1.)); - // look for gaps - if (has_gap_fill) - // not using safety offset here would "detect" very narrow gaps - // (but still long enough to escape the area threshold) that gap fill - // won't be able to fill but we'd still remove from infill area - append(gaps, diff_ex( - offset(last, - float(0.5 * distance)), - offset(offsets, float(0.5 * distance + 10)))); // safety offset - } - if (offsets.empty() && offsets_with_smaller_width.empty()) { - // Store the number of loops actually generated. - loop_number = i - 1; - // No region left to be filled in. - last.clear(); - break; - } else if (i > loop_number) { - // If i > loop_number, we were looking just for gaps. - break; - } - { - for (const ExPolygon& expolygon : offsets) { - // Outer contour may overlap with an inner contour, - // inner contour may overlap with another inner contour, - // outer contour may overlap with itself. - //FIXME evaluate the overlaps, annotate each point with an overlap depth, - // compensate for the depth of intersection. - contours[i].emplace_back(expolygon.contour, i, true); + //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; - if (!expolygon.holes.empty()) { - holes[i].reserve(holes[i].size() + expolygon.holes.size()); - for (const Polygon& hole : expolygon.holes) - holes[i].emplace_back(hole, i, false); - } - } + // Set one perimeter when TopSurfaces is selected. + if (config->only_one_wall_top) + loop_number = 0; - //BBS: save perimeter loop which use smaller width - if (i == 0) { - for (const ExPolygon& expolygon : offsets_with_smaller_width) { - contours[i].emplace_back(PerimeterGeneratorLoop(expolygon.contour, i, true, true)); - if (!expolygon.holes.empty()) { - holes[i].reserve(holes[i].size() + expolygon.holes.size()); - for (const Polygon& hole : expolygon.holes) - holes[i].emplace_back(PerimeterGeneratorLoop(hole, i, false, true)); - } - } - } - } + 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()); - last = std::move(offsets); + // 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); - //BBS: refer to superslicer - //store surface for top infill if only_one_wall_top - if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) { - this->split_top_surfaces(last, top_fills, last, fill_clip); - } + // 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(); - if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) { - // The last run of this loop is executed to collect gaps for gap fill. - // As the gap fill is either disabled or not - break; + // 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); } - } - // nest loops: holes first - for (int d = 0; d <= loop_number; ++ d) { - PerimeterGeneratorLoops &holes_d = holes[d]; - // loop through all holes having depth == d - for (int i = 0; i < (int)holes_d.size(); ++ i) { - const PerimeterGeneratorLoop &loop = holes_d[i]; - // find the hole loop that contains this one, if any - for (int t = d + 1; t <= loop_number; ++ t) { - for (int j = 0; j < (int)holes[t].size(); ++ j) { - PerimeterGeneratorLoop &candidate_parent = holes[t][j]; - if (candidate_parent.polygon.contains(loop.polygon.first_point())) { - candidate_parent.children.push_back(loop); - holes_d.erase(holes_d.begin() + i); - -- i; - goto NEXT_LOOP; - } - } - } - // if no hole contains this hole, find the contour loop that contains it - for (int t = loop_number; t >= 0; -- t) { - for (int j = 0; j < (int)contours[t].size(); ++ j) { - PerimeterGeneratorLoop &candidate_parent = contours[t][j]; - if (candidate_parent.polygon.contains(loop.polygon.first_point())) { - candidate_parent.children.push_back(loop); - holes_d.erase(holes_d.begin() + i); - -- i; - goto NEXT_LOOP; - } - } + // 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; } - NEXT_LOOP: ; } + + 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()); } - // nest contour loops - for (int d = loop_number; d >= 1; -- d) { - PerimeterGeneratorLoops &contours_d = contours[d]; - // loop through all contours having depth == d - for (int i = 0; i < (int)contours_d.size(); ++ i) { - const PerimeterGeneratorLoop &loop = contours_d[i]; - // find the contour loop that contains it - for (int t = d - 1; t >= 0; -- t) { - for (size_t j = 0; j < contours[t].size(); ++ j) { - PerimeterGeneratorLoop &candidate_parent = contours[t][j]; - if (candidate_parent.polygon.contains(loop.polygon.first_point())) { - candidate_parent.children.push_back(loop); - contours_d.erase(contours_d.begin() + i); - -- i; - goto NEXT_CONTOUR; - } - } - } - NEXT_CONTOUR: ; - } - } - // at this point, all loops should be in contours[0] - 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; - } - ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole); - // 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(entities, 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); - } + } + //PS - // if brim will be printed, reverse the order of perimeters so that - // we continue inwards after having finished the brim - // TODO: add test for perimeter order - bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; - if (is_outer_wall_first || - //BBS: always print outer wall first when there indeed has brim. - (this->layer_id == 0 && - this->object_config->brim_type == BrimType::btOuterOnly && - this->object_config->brim_width.value > 0)) - entities.reverse(); - // Orca: sandwich mode. Apply after 1st layer. - else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){ - entities.reverse(); // reverse all entities - order them from external to internal - if(entities.entities.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 - - // Initiate reorder sequence to bring any index 1 (first internal) perimeters ahead of any second internal perimeters - // Leaving these out of order will result in print defects on the external wall as they will be extruded prior to any - // external wall. To do the re-ordering, we are creating two extrusion arrays - reordered_extrusions which will contain - // the reordered extrusions and skipped_extrusions will contain the ones that were skipped in the scan - ExtrusionEntityCollection reordered_extrusions, skipped_extrusions; - bool found_second_internal = false; // helper variable to indicate the start of a new island - - for(auto extrusion_to_reorder : entities.entities){ //scan the perimeters to reorder - switch (extrusion_to_reorder->inset_idx) { + 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(found_second_internal){ //new island - move skipped extrusions to reordered array - for(auto extrusion_skipped : skipped_extrusions) - reordered_extrusions.append(*extrusion_skipped); - skipped_extrusions.clear(); - } - reordered_extrusions.append(*extrusion_to_reorder); + if (outer == -1) + outer = arr_i; break; - case 1: // first internal perimeter - reordered_extrusions.append(*extrusion_to_reorder); + case 1: // first internal wall + if (first_internal==-1 && arr_i>outer && outer!=-1){ + first_internal = arr_i; + } break; - default: // second internal+ perimeter -> put them in the skipped extrusions array - skipped_extrusions.append(*extrusion_to_reorder); - found_second_internal = true; + case 2: // second internal wall + if (second_internal == -1 && arr_i > first_internal && outer!=-1){ + second_internal = arr_i; + } break; } - } - if(entities.entities.size()>reordered_extrusions.size()){ - // we didnt find any more islands, so lets move the remaining skipped perimeters to the reordered extrusions list. - for(auto extrusion_skipped : skipped_extrusions) - reordered_extrusions.append(*extrusion_skipped); - skipped_extrusions.clear(); + 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 + } } - // 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 - for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { - switch (reordered_extrusions.entities[arr_i]->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 && second_internal>-1 && reordered_extrusions.entities[arr_i]->inset_idx == 0){ // found a new external perimeter after we've found all three perimeters 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++; } } - - if (outer > -1 && first_internal > -1 && second_internal > -1) { // found perimeters to re-order? - ExtrusionEntityCollection inner_outer_extrusions; // temporary collection to hold extrusions for reordering - - 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){ - inner_outer_extrusions.append(*reordered_extrusions.entities[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 - inner_outer_extrusions.append(*reordered_extrusions.entities[arr_j]); - } - - for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array - entities.replace(arr_j, *inner_outer_extrusions.entities[arr_j-position]); - } else - break; - // go to the next perimeter from the current position to continue scanning for external walls in the same island - position = arr_i + 1; + + 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; } } - - // append perimeters for this slice as a collection - if (! entities.empty()) - this->loops->append(entities); + } + + 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); + } - } // for each loop of an island + const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing; - // fill gaps - if (! gaps.empty()) { - // collapse - double min = 0.2 * perimeter_width * (1 - INSET_OVERLAP_TOLERANCE); - double max = 2. * perimeter_spacing; - ExPolygons gaps_ex = diff_ex( - //FIXME offset2 would be enough and cheaper. - opening_ex(gaps, float(min / 2.)), - offset2_ex(gaps, - float(max / 2.), float(max / 2. + ClipperSafetyOffset))); - ThickPolylines polylines; - for (ExPolygon& ex : gaps_ex) { - //BBS: Use DP simplify to avoid duplicated points and accelerate medial-axis calculation as well. - ex.douglas_peucker(surface_simplify_resolution); - ex.medial_axis(min, max, &polylines); - } - -#ifdef GAPS_OF_PERIMETER_DEBUG_TO_SVG - { - static int irun = 0; - BoundingBox bbox_svg; - bbox_svg.merge(get_extents(gaps_ex)); - { - std::stringstream stri; - stri << "debug_gaps_ex_" << irun << ".svg"; - SVG svg(stri.str(), bbox_svg); - svg.draw(to_polylines(gaps_ex), "blue", 0.5); - svg.Close(); - } - ++ irun; - } -#endif - // SoftFever: filter out tiny gap fills - polylines.erase(std::remove_if(polylines.begin(), polylines.end(), - [&](const ThickPolyline& p) { - return p.length() < scale_(config->filter_out_gap_fill.value); - }), polylines.end()); - - - if (! polylines.empty()) { - ExtrusionEntityCollection gap_fill; - variable_width(polylines, erGapFill, this->solid_infill_flow, gap_fill.entities); - /* Make sure we don't infill narrow parts that are already gap-filled - (we only consider this surface's gaps to reduce the diff() complexity). - Growing actual extrusions ensures that gaps not filled by medial axis - are not subtracted from fill surfaces (they might be too short gaps - that medial axis skips but infill might join with other infill regions - and use zigzag). */ - //FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing, - // therefore it may cover the area, but no the volume. - last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f)); - this->gap_fill->append(std::move(gap_fill.entities)); - - } - } + 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 = + coord_t inset = (loop_number < 0) ? 0 : (loop_number == 0) ? - // one loop - ext_perimeter_spacing / 2 : - // two or more loops? - perimeter_spacing / 2; + // 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)))); - // only apply infill overlap if we actually have one perimeter - coord_t infill_peri_overlap = 0; - coord_t top_infill_peri_overlap = 0; - if (inset > 0) { - if(this->layer_id == 0 || this->upper_slices == nullptr){ - infill_peri_overlap = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); - }else{ - infill_peri_overlap = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); - top_infill_peri_overlap = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); - } - inset -= infill_peri_overlap; - } // simplify infill contours according to resolution Polygons pp; - for (ExPolygon &ex : last) + for (ExPolygon& ex : infill_contour) ex.simplify_p(m_scaled_resolution, &pp); ExPolygons not_filled_exp = union_ex(pp); // collapse too narrow infill areas - coord_t min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); + const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); ExPolygons infill_exp = offset2_ex( not_filled_exp, - float(-inset - min_perimeter_infill_spacing / 2.), - float(min_perimeter_infill_spacing / 2.)); + float(-min_perimeter_infill_spacing / 2.), + float(inset + min_perimeter_infill_spacing / 2.)); // append infill areas to fill_surfaces - //if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill - ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2))); - if (!top_fills.empty()) { - infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap))); + if (!top_expolygons.empty()) { + infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset))); } this->fill_surfaces->append(infill_exp, stInternal); @@ -2421,861 +2376,813 @@ void PerimeterGenerator::process_classic() // BBS: get the no-overlap infill expolygons { ExPolygons polyWithoutOverlap; - if (min_perimeter_infill_spacing / 2 > infill_peri_overlap) - polyWithoutOverlap = offset2_ex( - not_filled_exp, - float(-inset - min_perimeter_infill_spacing / 2.), - float(min_perimeter_infill_spacing / 2 - infill_peri_overlap)); - else - polyWithoutOverlap = offset_ex( - not_filled_exp, - double(-inset - infill_peri_overlap)); - if (!top_fills.empty()) - polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp); + 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()); } - - } // for each island + } } -//BBS: -void PerimeterGenerator::add_infill_contour_for_arachne( ExPolygons infill_contour, - int loops, - coord_t ext_perimeter_spacing, - coord_t perimeter_spacing, - coord_t min_perimeter_infill_spacing, - coord_t spacing, - bool is_inner_part) +void PerimeterGenerator::process_classic() { - 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 insert = (loops < 0) ? 0: ext_perimeter_spacing; - if (is_inner_part || loops > 0) - insert = perimeter_spacing; + group_region_by_fuzzify(*this); - insert = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(insert)))); + // other perimeters + m_mm3_per_mm = this->perimeter_flow.mm3_per_mm(); + coord_t perimeter_width = this->perimeter_flow.scaled_width(); + coord_t perimeter_spacing = this->perimeter_flow.scaled_spacing(); - Polygons inner_pp; - for (ExPolygon &ex : infill_contour) - ex.simplify_p(m_scaled_resolution, &inner_pp); + // 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(); - this->fill_surfaces->append(offset2_ex(union_ex(inner_pp), float(-min_perimeter_infill_spacing / 2.), float(insert + min_perimeter_infill_spacing / 2.)), stInternal); + coord_t ext_perimeter_spacing = this->ext_perimeter_flow.scaled_spacing(); + coord_t ext_perimeter_spacing2; + // Orca: ignore precise_outer_wall if wall_sequence is not InnerOuter + if(config->precise_outer_wall && this->config->wall_sequence == WallSequence::InnerOuter) + ext_perimeter_spacing2 = scaled(0.5f * (this->ext_perimeter_flow.width() + this->perimeter_flow.width())); + else + 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(); - append(*this->fill_no_overlap, offset2_ex(union_ex(inner_pp), float(-min_perimeter_infill_spacing / 2.), float(+min_perimeter_infill_spacing / 2.))); -} + // 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))); + } + + // Calculate the minimum required spacing between two adjacent traces. + // This should be equal to the nominal flow spacing but we experiment + // with some tolerance in order to avoid triggering medial axis when + // some squishing might work. Loops are still spaced by the entire + // flow spacing; this only applies to collapsing parts. + // For ext_min_spacing we use the ext_perimeter_spacing calculated for two adjacent + // external loops (which is the correct way) instead of using ext_perimeter_spacing2 + // which is the spacing between external and internal, which is not correct + // and would make the collapsing (thus the details resolution) dependent on + // internal flow which is unrelated. + coord_t min_spacing = coord_t(perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE)); + coord_t ext_min_spacing = coord_t(ext_perimeter_spacing * (1 - INSET_OVERLAP_TOLERANCE)); + bool has_gap_fill = this->config->gap_infill_speed.value > 0; -// Orca: sacrificial bridge layer algorithm ported from SuperSlicer -void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perimeter_spacing, coord_t ext_perimeter_width) -{ - //store surface for bridge infill to avoid unsupported perimeters (but the first one, this one is always good) - if (this->config->counterbore_hole_bridging != chbNone - && this->lower_slices != NULL && !this->lower_slices->empty()) { - const coordf_t bridged_infill_margin = scale_(BRIDGE_INFILL_MARGIN); + // BBS: this flow is for smaller external perimeter for small area + coord_t ext_min_spacing_smaller = coord_t(ext_perimeter_spacing * (1 - SMALLER_EXT_INSET_OVERLAP_TOLERANCE)); + this->smaller_ext_perimeter_flow = this->ext_perimeter_flow; + // BBS: to be checked + this->smaller_ext_perimeter_flow = this->smaller_ext_perimeter_flow.with_width(SCALING_FACTOR * + (ext_perimeter_width - 0.5 * SMALLER_EXT_INSET_OVERLAP_TOLERANCE * ext_perimeter_spacing)); + m_ext_mm3_per_mm_smaller_width = this->smaller_ext_perimeter_flow.mm3_per_mm(); - for (size_t surface_idx = 0; surface_idx < all_surfaces.size(); surface_idx++) { - Surface* surface = &all_surfaces[surface_idx]; - ExPolygons last = { surface->expolygon }; - //compute our unsupported surface - ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes); - if (!unsupported.empty()) { - //remove small overhangs - ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing)); - if (!unsupported_filtered.empty()) { - //to_draw.insert(to_draw.end(), last.begin(), last.end()); - //extract only the useful part of the lower layer. The safety offset is really needed here. - ExPolygons support = diff_ex(last, unsupported, ApplySafetyOffset::Yes); - if (!unsupported.empty()) { - //only consider the part that can be bridged (really, by the bridge algorithm) - //first, separate into islands (ie, each ExPlolygon) - int numploy = 0; - //only consider the bottom layer that intersect unsupported, to be sure it's only on our island. - ExPolygonCollection lower_island(support); - //a detector per island - ExPolygons bridgeable; - for (ExPolygon unsupported : unsupported_filtered) { - BridgeDetector detector{ unsupported, - lower_island.expolygons, - perimeter_spacing }; - if (detector.detect_angle(Geometry::deg2rad(this->config->bridge_angle.value))) - expolygons_append(bridgeable, union_ex(detector.coverage(-1, true))); - } - if (!bridgeable.empty()) { - //check if we get everything or just the bridgeable area - if (/*this->config->counterbore_hole_bridging.value == chbNoPeri || */this->config->counterbore_hole_bridging.value == chbFilled) { - //we bridge everything, even the not-bridgeable bits - for (size_t i = 0; i < unsupported_filtered.size();) { - ExPolygon& poly_unsupp = *(unsupported_filtered.begin() + i); - Polygons contour_simplified = poly_unsupp.contour.simplify(perimeter_spacing); - ExPolygon poly_unsupp_bigger = poly_unsupp; - Polygons contour_bigger = offset(poly_unsupp_bigger.contour, bridged_infill_margin); - if (contour_bigger.size() == 1) poly_unsupp_bigger.contour = contour_bigger[0]; - - //check convex, has some bridge, not overhang - if (contour_simplified.size() == 1 && contour_bigger.size() == 1 && contour_simplified[0].concave_points().size() == 0 - && intersection_ex(bridgeable, ExPolygons{ poly_unsupp }).size() > 0 - && diff_ex(ExPolygons{ poly_unsupp_bigger }, union_ex(last, offset_ex(bridgeable, bridged_infill_margin + perimeter_spacing / 2)), ApplySafetyOffset::Yes).size() == 0 - ) { - //ok, keep it - i++; - } else { - unsupported_filtered.erase(unsupported_filtered.begin() + i); - } - } - unsupported_filtered = intersection_ex(last, - offset2_ex(unsupported_filtered, double(-perimeter_spacing / 2), double(bridged_infill_margin + perimeter_spacing / 2))); - if (this->config->counterbore_hole_bridging.value == chbFilled) { - for (ExPolygon& expol : unsupported_filtered) { - //check if the holes won't be covered by the upper layer - //TODO: if we want to do that, we must modify the geometry before making perimeters. - //if (this->upper_slices != nullptr && !this->upper_slices->expolygons.empty()) { - // for (Polygon &poly : expol.holes) poly.make_counter_clockwise(); - // float perimeterwidth = this->config->perimeters == 0 ? 0 : (this->ext_perimeter_flow.scaled_width() + (this->config->perimeters - 1) + this->perimeter_flow.scaled_spacing()); - // std::cout << "test upper slices with perimeterwidth=" << perimeterwidth << "=>" << offset_ex(this->upper_slices->expolygons, -perimeterwidth).size(); - // if (intersection(Polygons() = { expol.holes }, to_polygons(offset_ex(this->upper_slices->expolygons, -this->ext_perimeter_flow.scaled_width() / 2))).empty()) { - // std::cout << " EMPTY"; - // expol.holes.clear(); - // } else { - // } - // std::cout << "\n"; - //} else { - expol.holes.clear(); - //} - - //detect inside volume - for (size_t surface_idx_other = 0; surface_idx_other < all_surfaces.size(); surface_idx_other++) { - if (surface_idx == surface_idx_other) continue; - if (intersection_ex(ExPolygons() = { expol }, ExPolygons() = { all_surfaces[surface_idx_other].expolygon }).size() > 0) { - //this means that other_surf was inside an expol holes - //as we removed them, we need to add a new one - ExPolygons new_poly = offset2_ex(ExPolygons{ all_surfaces[surface_idx_other].expolygon }, double(-bridged_infill_margin - perimeter_spacing), double(perimeter_spacing)); - if (new_poly.size() == 1) { - all_surfaces[surface_idx_other].expolygon = new_poly[0]; - expol.holes.push_back(new_poly[0].contour); - expol.holes.back().make_clockwise(); - } else { - for (size_t idx = 0; idx < new_poly.size(); idx++) { - Surface new_surf = all_surfaces[surface_idx_other]; - new_surf.expolygon = new_poly[idx]; - all_surfaces.push_back(new_surf); - expol.holes.push_back(new_poly[idx].contour); - expol.holes.back().make_clockwise(); - } - all_surfaces.erase(all_surfaces.begin() + surface_idx_other); - if (surface_idx_other < surface_idx) { - surface_idx--; - surface = &all_surfaces[surface_idx]; - } - surface_idx_other--; - } - } - } - } - - } - //TODO: add other polys as holes inside this one (-margin) - } else if (/*this->config->counterbore_hole_bridging.value == chbBridgesOverhangs || */this->config->counterbore_hole_bridging.value == chbBridges) { - //simplify to avoid most of artefacts from printing lines. - ExPolygons bridgeable_simplified; - for (ExPolygon& poly : bridgeable) { - poly.simplify(perimeter_spacing, &bridgeable_simplified); - } - bridgeable_simplified = offset2_ex(bridgeable_simplified, -ext_perimeter_width, ext_perimeter_width); - //bridgeable_simplified = intersection_ex(bridgeable_simplified, unsupported_filtered); - //offset by perimeter spacing because the simplify may have reduced it a bit. - //it's not dangerous as it will be intersected by 'unsupported' later - //FIXME: add overlap in this->fill_surfaces->append - //FIXME: it overlap inside unsuppported not-bridgeable area! - - //bridgeable_simplified = offset2_ex(bridgeable_simplified, (double)-perimeter_spacing, (double)perimeter_spacing * 2); - //ExPolygons unbridgeable = offset_ex(diff_ex(unsupported, bridgeable_simplified), perimeter_spacing * 3 / 2); - //ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2))); - //unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width); - - - // if (this->config->counterbore_hole_bridging.value == chbBridges) { - ExPolygons unbridgeable = unsupported_filtered; - for (ExPolygon& expol : unbridgeable) - expol.holes.clear(); - unbridgeable = diff_ex(unbridgeable, bridgeable_simplified); - unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width * 2, ext_perimeter_width * 2); - ExPolygons bridges_temp = offset2_ex(intersection_ex(last, diff_ex(unsupported_filtered, unbridgeable), ApplySafetyOffset::Yes), -ext_perimeter_width / 4, ext_perimeter_width / 4); - //remove the overhangs section from the surface polygons - ExPolygons reference = last; - last = diff_ex(last, unsupported_filtered); - //ExPolygons no_bridge = diff_ex(offset_ex(unbridgeable, ext_perimeter_width * 3 / 2), last); - //bridges_temp = diff_ex(bridges_temp, no_bridge); - coordf_t offset_to_do = bridged_infill_margin; - bool first = true; - unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width)); - while (offset_to_do > ext_perimeter_width * 1.5) { - unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width / 4, ext_perimeter_width * 2.25, ClipperLib::jtSquare); - bridges_temp = diff_ex(bridges_temp, unbridgeable); - bridges_temp = offset_ex(bridges_temp, ext_perimeter_width, ClipperLib::jtMiter, 6.); - unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width)); - offset_to_do -= ext_perimeter_width; - first = false; - } - unbridgeable = offset_ex(unbridgeable, ext_perimeter_width + offset_to_do, ClipperLib::jtSquare); - bridges_temp = diff_ex(bridges_temp, unbridgeable); - unsupported_filtered = offset_ex(bridges_temp, offset_to_do); - unsupported_filtered = intersection_ex(unsupported_filtered, reference); - // } else { - // ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2))); - // unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width); - // unsupported_filtered = unbridgeable; - - // ////put the bridge area inside the unsupported_filtered variable - // //unsupported_filtered = intersection_ex(last, - // // diff_ex( - // // offset_ex(bridgeable_simplified, (double)perimeter_spacing / 2), - // // unbridgeable - // // ) - // // ); - // } - } else { - unsupported_filtered.clear(); - } - } else { - unsupported_filtered.clear(); - } - } - - if (!unsupported_filtered.empty()) { - - //add this directly to the infill list. - // this will avoid to throw wrong offsets into a good polygons - this->fill_surfaces->append( - unsupported_filtered, - stInternal); - - // store the results - last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes); - //remove "thin air" polygons (note: it assumes that all polygons below will be extruded) - for (int i = 0; i < last.size(); i++) { - if (intersection_ex(support, ExPolygons() = { last[i] }).empty()) { - this->fill_surfaces->append( - ExPolygons() = { last[i] }, - stInternal); - last.erase(last.begin() + i); - i--; - } - } - } - } - } - if (last.size() == 0) { - all_surfaces.erase(all_surfaces.begin() + surface_idx); - surface_idx--; - } else { - surface->expolygon = last[0]; - for (size_t idx = 1; idx < last.size(); idx++) { - all_surfaces.emplace_back(*surface, last[idx]); - } - } - } - } -} - -// 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; + // prepare grown lower layer slices for overhang detection + m_lower_polygons_series = generate_lower_polygons_series(this->perimeter_flow.width()); + m_lower_overhang_dist_boundary = dist_boundary(this->perimeter_flow.width()); + if (ext_perimeter_width == perimeter_width){ + m_external_lower_polygons_series = m_lower_polygons_series; + m_external_overhang_dist_boundary=m_lower_overhang_dist_boundary; + } else { + m_external_lower_polygons_series = generate_lower_polygons_series(this->ext_perimeter_flow.width()); + m_external_overhang_dist_boundary = dist_boundary(this->ext_perimeter_flow.width()); + } + m_smaller_external_lower_polygons_series = generate_lower_polygons_series(this->smaller_ext_perimeter_flow.width()); + m_smaller_external_overhang_dist_boundary = dist_boundary(this->smaller_ext_perimeter_flow.width()); + // we need to process each island separately because we might have different + // extra perimeters for each one + 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; + //BBS: reorder the surface to reduce the travel time + ExPolygons surface_exp; + for (const Surface &surface : all_surfaces) + surface_exp.push_back(surface.expolygon); + std::vector surface_order = chain_expolygons(surface_exp); + for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) { + const Surface &surface = all_surfaces[surface_order[order_idx]]; // 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 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) + if (this->layer_id == object_config->raft_layers && 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) + // Set the topmost layer to be one wall + if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr) 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); + ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution)); + ExPolygons gaps; + ExPolygons top_fills; + ExPolygons fill_clip; + if (loop_number >= 0) { + // In case no perimeters are to be generated, loop_number will equal to -1. + std::vector contours(loop_number+1); // depth => loops + std::vector holes(loop_number+1); // depth => loops + ThickPolylines thin_walls; + // we loop one time more than needed in order to find gaps after the last perimeter was applied + for (int i = 0;; ++ i) { // outer loop is 0 + // Calculate next onion shell of perimeters. + ExPolygons offsets; + ExPolygons offsets_with_smaller_width; + if (i == 0) { + // look for thin walls + if (this->config->detect_thin_wall) { + // the minimum thickness of a single loop is: + // ext_width/2 + ext_spacing/2 + spacing/2 + width/2 + offsets = offset2_ex(last, + -float(ext_perimeter_width / 2. + ext_min_spacing / 2. - 1), + +float(ext_min_spacing / 2. - 1)); + // the following offset2 ensures almost nothing in @thin_walls is narrower than $min_width + // (actually, something larger than that still may exist due to mitering or other causes) + coord_t min_width = coord_t(scale_(this->ext_perimeter_flow.nozzle_diameter() / 3)); + ExPolygons expp = opening_ex( + // medial axis requires non-overlapping geometry + diff_ex(last, offset(offsets, float(ext_perimeter_width / 2.) + ClipperSafetyOffset)), + float(min_width / 2.)); + // the maximum thickness of our thin wall area is equal to the minimum thickness of a single loop + for (ExPolygon &ex : expp) + ex.medial_axis(min_width, ext_perimeter_width + ext_perimeter_spacing2, &thin_walls); + } else { + coord_t ext_perimeter_smaller_width = this->smaller_ext_perimeter_flow.scaled_width(); + for (const ExPolygon& expolygon : last) { + // BBS: judge whether it's narrow but not too long island which is hard to place two line + ExPolygons expolys; + expolys.push_back(expolygon); + ExPolygons offset_result = offset2_ex(expolys, + -float(ext_perimeter_width / 2. + ext_min_spacing_smaller / 2.), + +float(ext_min_spacing_smaller / 2.)); + if (offset_result.empty() && + expolygon.area() < (double)(ext_perimeter_width + ext_min_spacing_smaller) * scale_(narrow_loop_length_threshold)) { + // BBS: for narrow external loop, use smaller line width + ExPolygons temp_result = offset_ex(expolygon, -float(ext_perimeter_smaller_width / 2.)); + offsets_with_smaller_width.insert(offsets_with_smaller_width.end(), temp_result.begin(), temp_result.end()); + } + else { + //BBS: for not narrow loop, use normal external perimeter line width + ExPolygons temp_result = offset_ex(expolygon, -float(ext_perimeter_width / 2.)); + offsets.insert(offsets.end(), temp_result.begin(), temp_result.end()); + } + } + } + if (m_spiral_vase && (offsets.size() > 1 || offsets_with_smaller_width.size() > 1)) { + // Remove all but the largest area polygon. + keep_largest_contour_only(offsets); + //BBS + if (offsets.empty()) + //BBS: only have small width loop, then keep the largest in spiral vase mode + keep_largest_contour_only(offsets_with_smaller_width); + else + //BBS: have large area, clean the small width loop + offsets_with_smaller_width.clear(); + } + } else { + //FIXME Is this offset correct if the line width of the inner perimeters differs + // from the line width of the infill? + coord_t distance = (i == 1) ? ext_perimeter_spacing2 : perimeter_spacing; + //BBS + //offsets = this->config->thin_walls ? + // This path will ensure, that the perimeters do not overfill, as in + // prusa3d/Slic3r GH #32, but with the cost of rounding the perimeters + // excessively, creating gaps, which then need to be filled in by the not very + // reliable gap fill algorithm. + // Also the offset2(perimeter, -x, x) may sometimes lead to a perimeter, which is larger than + // the original. + //offset2_ex(last, + // - float(distance + min_spacing / 2. - 1.), + // float(min_spacing / 2. - 1.)) : + // If "detect thin walls" is not enabled, this paths will be entered, which + // leads to overflows, as in prusa3d/Slic3r GH #32 + //offset_ex(last, - float(distance)); - //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; + //BBS: For internal perimeter, we should "enable" thin wall strategy in which offset2 is used to + // remove too closed line, so that gap fill can be used for such internal narrow area in following + // handling. + offsets = offset2_ex(last, + -float(distance + min_spacing / 2. - 1.), + float(min_spacing / 2. - 1.)); + // look for gaps + if (has_gap_fill) + // not using safety offset here would "detect" very narrow gaps + // (but still long enough to escape the area threshold) that gap fill + // won't be able to fill but we'd still remove from infill area + append(gaps, diff_ex( + offset(last, - float(0.5 * distance)), + offset(offsets, float(0.5 * distance + 10)))); // safety offset + } + if (offsets.empty() && offsets_with_smaller_width.empty()) { + // Store the number of loops actually generated. + loop_number = i - 1; + // No region left to be filled in. + last.clear(); + break; + } else if (i > loop_number) { + // If i > loop_number, we were looking just for gaps. + break; + } + { + for (const ExPolygon& expolygon : offsets) { + // Outer contour may overlap with an inner contour, + // inner contour may overlap with another inner contour, + // outer contour may overlap with itself. + //FIXME evaluate the overlaps, annotate each point with an overlap depth, + // compensate for the depth of intersection. + contours[i].emplace_back(expolygon.contour, i, true); - // Set one perimeter when TopSurfaces is selected. - if (config->only_one_wall_top) - loop_number = 0; + if (!expolygon.holes.empty()) { + holes[i].reserve(holes[i].size() + expolygon.holes.size()); + for (const Polygon& hole : expolygon.holes) + holes[i].emplace_back(hole, i, false); + } + } - 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()); + //BBS: save perimeter loop which use smaller width + if (i == 0) { + for (const ExPolygon& expolygon : offsets_with_smaller_width) { + contours[i].emplace_back(PerimeterGeneratorLoop(expolygon.contour, i, true, true)); + if (!expolygon.holes.empty()) { + holes[i].reserve(holes[i].size() + expolygon.holes.size()); + for (const Polygon& hole : expolygon.holes) + holes[i].emplace_back(PerimeterGeneratorLoop(hole, i, false, true)); + } + } + } + } - // 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); + last = std::move(offsets); - // 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(); + //BBS: refer to superslicer + //store surface for top infill if only_one_wall_top + if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) { + this->split_top_surfaces(last, top_fills, last, fill_clip); + } - // 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 (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) { + // The last run of this loop is executed to collect gaps for gap fill. + // As the gap fill is either disabled or not + break; + } + } - 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); + // nest loops: holes first + for (int d = 0; d <= loop_number; ++ d) { + PerimeterGeneratorLoops &holes_d = holes[d]; + // loop through all holes having depth == d + for (int i = 0; i < (int)holes_d.size(); ++ i) { + const PerimeterGeneratorLoop &loop = holes_d[i]; + // find the hole loop that contains this one, if any + for (int t = d + 1; t <= loop_number; ++ t) { + for (int j = 0; j < (int)holes[t].size(); ++ j) { + PerimeterGeneratorLoop &candidate_parent = holes[t][j]; + if (candidate_parent.polygon.contains(loop.polygon.first_point())) { + candidate_parent.children.push_back(loop); + holes_d.erase(holes_d.begin() + i); + -- i; + goto NEXT_LOOP; + } + } + } + // if no hole contains this hole, find the contour loop that contains it + for (int t = loop_number; t >= 0; -- t) { + for (int j = 0; j < (int)contours[t].size(); ++ j) { + PerimeterGeneratorLoop &candidate_parent = contours[t][j]; + if (candidate_parent.polygon.contains(loop.polygon.first_point())) { + candidate_parent.children.push_back(loop); + holes_d.erase(holes_d.begin() + i); + -- i; + goto NEXT_LOOP; + } + } + } + NEXT_LOOP: ; + } + } + // nest contour loops + for (int d = loop_number; d >= 1; -- d) { + PerimeterGeneratorLoops &contours_d = contours[d]; + // loop through all contours having depth == d + for (int i = 0; i < (int)contours_d.size(); ++ i) { + const PerimeterGeneratorLoop &loop = contours_d[i]; + // find the contour loop that contains it + for (int t = d - 1; t >= 0; -- t) { + for (size_t j = 0; j < contours[t].size(); ++ j) { + PerimeterGeneratorLoop &candidate_parent = contours[t][j]; + if (candidate_parent.polygon.contains(loop.polygon.first_point())) { + candidate_parent.children.push_back(loop); + contours_d.erase(contours_d.begin() + i); + -- i; + goto NEXT_CONTOUR; + } + } + } + NEXT_CONTOUR: ; + } + } + // at this point, all loops should be in contours[0] + 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; + } + ExtrusionEntityCollection entities = traverse_loops(*this, contours.front(), thin_walls, steep_overhang_contour, steep_overhang_hole); + // 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(entities, 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); + } - // Remove bridges from top surface polygons. - top_expolygons = diff_ex(top_expolygons, current_slices_bridges); + // if brim will be printed, reverse the order of perimeters so that + // we continue inwards after having finished the brim + // TODO: add test for perimeter order + bool is_outer_wall_first = this->config->wall_sequence == WallSequence::OuterInner; + if (is_outer_wall_first || + //BBS: always print outer wall first when there indeed has brim. + (this->layer_id == 0 && + this->object_config->brim_type == BrimType::btOuterOnly && + this->object_config->brim_width.value > 0)) + entities.reverse(); + // Orca: sandwich mode. Apply after 1st layer. + else if ((this->config->wall_sequence == WallSequence::InnerOuterInner) && layer_id > 0){ + entities.reverse(); // reverse all entities - order them from external to internal + if(entities.entities.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 + + // Initiate reorder sequence to bring any index 1 (first internal) perimeters ahead of any second internal perimeters + // Leaving these out of order will result in print defects on the external wall as they will be extruded prior to any + // external wall. To do the re-ordering, we are creating two extrusion arrays - reordered_extrusions which will contain + // the reordered extrusions and skipped_extrusions will contain the ones that were skipped in the scan + ExtrusionEntityCollection reordered_extrusions, skipped_extrusions; + bool found_second_internal = false; // helper variable to indicate the start of a new island + + for(auto extrusion_to_reorder : entities.entities){ //scan the perimeters to reorder + switch (extrusion_to_reorder->inset_idx) { + case 0: // external perimeter + if(found_second_internal){ //new island - move skipped extrusions to reordered array + for(auto extrusion_skipped : skipped_extrusions) + reordered_extrusions.append(*extrusion_skipped); + skipped_extrusions.clear(); + } + reordered_extrusions.append(*extrusion_to_reorder); + break; + case 1: // first internal perimeter + reordered_extrusions.append(*extrusion_to_reorder); + break; + default: // second internal+ perimeter -> put them in the skipped extrusions array + skipped_extrusions.append(*extrusion_to_reorder); + found_second_internal = true; + break; + } + } + if(entities.entities.size()>reordered_extrusions.size()){ + // we didnt find any more islands, so lets move the remaining skipped perimeters to the reordered extrusions list. + for(auto extrusion_skipped : skipped_extrusions) + reordered_extrusions.append(*extrusion_skipped); + skipped_extrusions.clear(); + } + + // 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 + for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { + switch (reordered_extrusions.entities[arr_i]->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 && second_internal>-1 && reordered_extrusions.entities[arr_i]->inset_idx == 0){ // found a new external perimeter after we've found all three perimeters 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 + } + } + + if (outer > -1 && first_internal > -1 && second_internal > -1) { // found perimeters to re-order? + ExtrusionEntityCollection inner_outer_extrusions; // temporary collection to hold extrusions for reordering + + 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){ + inner_outer_extrusions.append(*reordered_extrusions.entities[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 + inner_outer_extrusions.append(*reordered_extrusions.entities[arr_j]); + } + + for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array + entities.replace(arr_j, *inner_outer_extrusions.entities[arr_j-position]); + } else + break; + // go to the next perimeter from the current position to continue scanning for external walls in the same island + position = arr_i + 1; + } } + } + + // append perimeters for this slice as a collection + if (! entities.empty()) + this->loops->append(entities); - // 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)); + } // for each loop of an island - // 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); + // fill gaps + if (! gaps.empty()) { + // collapse + double min = 0.2 * perimeter_width * (1 - INSET_OVERLAP_TOLERANCE); + double max = 2. * perimeter_spacing; + ExPolygons gaps_ex = diff_ex( + //FIXME offset2 would be enough and cheaper. + opening_ex(gaps, float(min / 2.)), + offset2_ex(gaps, - float(max / 2.), float(max / 2. + ClipperSafetyOffset))); + ThickPolylines polylines; + for (ExPolygon& ex : gaps_ex) { + //BBS: Use DP simplify to avoid duplicated points and accelerate medial-axis calculation as well. + ex.douglas_peucker(surface_simplify_resolution); + ex.medial_axis(min, max, &polylines); + } + +#ifdef GAPS_OF_PERIMETER_DEBUG_TO_SVG + { + static int irun = 0; + BoundingBox bbox_svg; + bbox_svg.merge(get_extents(gaps_ex)); + { + std::stringstream stri; + stri << "debug_gaps_ex_" << irun << ".svg"; + SVG svg(stri.str(), bbox_svg); + svg.draw(to_polylines(gaps_ex), "blue", 0.5); + svg.Close(); + } + ++ irun; + } +#endif + // SoftFever: filter out tiny gap fills + polylines.erase(std::remove_if(polylines.begin(), polylines.end(), + [&](const ThickPolyline& p) { + return p.length() < scale_(config->filter_out_gap_fill.value); + }), polylines.end()); - // 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(); + if (! polylines.empty()) { + ExtrusionEntityCollection gap_fill; + variable_width(polylines, erGapFill, this->solid_infill_flow, gap_fill.entities); + /* Make sure we don't infill narrow parts that are already gap-filled + (we only consider this surface's gaps to reduce the diff() complexity). + Growing actual extrusions ensures that gaps not filled by medial axis + are not subtracted from fill surfaces (they might be too short gaps + that medial axis skips but infill might join with other infill regions + and use zigzag). */ + //FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing, + // therefore it may cover the area, but no the volume. + last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f)); + this->gap_fill->append(std::move(gap_fill.entities)); - // 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()); + // 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 / 2 : + // two or more loops? + perimeter_spacing / 2; + + // only apply infill overlap if we actually have one perimeter + coord_t infill_peri_overlap = 0; + coord_t top_infill_peri_overlap = 0; + if (inset > 0) { + if(this->layer_id == 0 || this->upper_slices == nullptr){ + infill_peri_overlap = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); + }else{ + infill_peri_overlap = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); + top_infill_peri_overlap = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale(inset + solid_infill_spacing / 2)))); } + inset -= infill_peri_overlap; } - //PS + // simplify infill contours according to resolution + Polygons pp; + for (ExPolygon &ex : last) + ex.simplify_p(m_scaled_resolution, &pp); + ExPolygons not_filled_exp = union_ex(pp); + // collapse too narrow infill areas + coord_t min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); - loop_number = int(perimeters.size()) - 1; + ExPolygons infill_exp = offset2_ex( + not_filled_exp, + float(-inset - min_perimeter_infill_spacing / 2.), + float(min_perimeter_infill_spacing / 2.)); + // append infill areas to fill_surfaces + //if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill + ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2))); + if (!top_fills.empty()) { + infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap))); + } + this->fill_surfaces->append(infill_exp, stInternal); - #ifdef ARACHNE_DEBUG + apply_extra_perimeters(infill_exp); + + // BBS: get the no-overlap infill expolygons { - 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())); + ExPolygons polyWithoutOverlap; + if (min_perimeter_infill_spacing / 2 > infill_peri_overlap) + polyWithoutOverlap = offset2_ex( + not_filled_exp, + float(-inset - min_perimeter_infill_spacing / 2.), + float(min_perimeter_infill_spacing / 2 - infill_peri_overlap)); + else + polyWithoutOverlap = offset_ex( + not_filled_exp, + double(-inset - infill_peri_overlap)); + if (!top_fills.empty()) + polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp); + this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end()); } -#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; - }()); + } // for each island +} + +//BBS: +void PerimeterGenerator::add_infill_contour_for_arachne( ExPolygons infill_contour, + int loops, + coord_t ext_perimeter_spacing, + coord_t perimeter_spacing, + coord_t min_perimeter_infill_spacing, + coord_t spacing, + bool is_inner_part) +{ + if( offset_ex(infill_contour, -float(spacing / 2.)).empty() ) + { + infill_contour.clear(); // Infill region is too small, so let's filter it out. + } - int start_perimeter = int(perimeters.size()) - 1; - int end_perimeter = -1; - int direction = -1; + // 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 insert = (loops < 0) ? 0: ext_perimeter_spacing; + if (is_inner_part || loops > 0) + insert = perimeter_spacing; - 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; - } + insert = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale(insert)))); - 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); - } + Polygons inner_pp; + for (ExPolygon &ex : infill_contour) + ex.simplify_p(m_scaled_resolution, &inner_pp); - // 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); - } + this->fill_surfaces->append(offset2_ex(union_ex(inner_pp), float(-min_perimeter_infill_spacing / 2.), float(insert + min_perimeter_infill_spacing / 2.)), stInternal); + + append(*this->fill_no_overlap, offset2_ex(union_ex(inner_pp), float(-min_perimeter_infill_spacing / 2.), float(+min_perimeter_infill_spacing / 2.))); +} + +// Orca: sacrificial bridge layer algorithm ported from SuperSlicer +void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perimeter_spacing, coord_t ext_perimeter_width) +{ + //store surface for bridge infill to avoid unsupported perimeters (but the first one, this one is always good) + if (this->config->counterbore_hole_bridging != chbNone + && this->lower_slices != NULL && !this->lower_slices->empty()) { + const coordf_t bridged_infill_margin = scale_(BRIDGE_INFILL_MARGIN); + + for (size_t surface_idx = 0; surface_idx < all_surfaces.size(); surface_idx++) { + Surface* surface = &all_surfaces[surface_idx]; + ExPolygons last = { surface->expolygon }; + //compute our unsupported surface + ExPolygons unsupported = diff_ex(last, *this->lower_slices, ApplySafetyOffset::Yes); + if (!unsupported.empty()) { + //remove small overhangs + ExPolygons unsupported_filtered = offset2_ex(unsupported, double(-perimeter_spacing), double(perimeter_spacing)); + if (!unsupported_filtered.empty()) { + //to_draw.insert(to_draw.end(), last.begin(), last.end()); + //extract only the useful part of the lower layer. The safety offset is really needed here. + ExPolygons support = diff_ex(last, unsupported, ApplySafetyOffset::Yes); + if (!unsupported.empty()) { + //only consider the part that can be bridged (really, by the bridge algorithm) + //first, separate into islands (ie, each ExPlolygon) + int numploy = 0; + //only consider the bottom layer that intersect unsupported, to be sure it's only on our island. + ExPolygonCollection lower_island(support); + //a detector per island + ExPolygons bridgeable; + for (ExPolygon unsupported : unsupported_filtered) { + BridgeDetector detector{ unsupported, + lower_island.expolygons, + perimeter_spacing }; + if (detector.detect_angle(Geometry::deg2rad(this->config->bridge_angle.value))) + expolygons_append(bridgeable, union_ex(detector.coverage(-1, true))); + } + if (!bridgeable.empty()) { + //check if we get everything or just the bridgeable area + if (/*this->config->counterbore_hole_bridging.value == chbNoPeri || */this->config->counterbore_hole_bridging.value == chbFilled) { + //we bridge everything, even the not-bridgeable bits + for (size_t i = 0; i < unsupported_filtered.size();) { + ExPolygon& poly_unsupp = *(unsupported_filtered.begin() + i); + Polygons contour_simplified = poly_unsupp.contour.simplify(perimeter_spacing); + ExPolygon poly_unsupp_bigger = poly_unsupp; + Polygons contour_bigger = offset(poly_unsupp_bigger.contour, bridged_infill_margin); + if (contour_bigger.size() == 1) poly_unsupp_bigger.contour = contour_bigger[0]; - 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()); + //check convex, has some bridge, not overhang + if (contour_simplified.size() == 1 && contour_bigger.size() == 1 && contour_simplified[0].concave_points().size() == 0 + && intersection_ex(bridgeable, ExPolygons{ poly_unsupp }).size() > 0 + && diff_ex(ExPolygons{ poly_unsupp_bigger }, union_ex(last, offset_ex(bridgeable, bridged_infill_margin + perimeter_spacing / 2)), ApplySafetyOffset::Yes).size() == 0 + ) { + //ok, keep it + i++; + } else { + unsupported_filtered.erase(unsupported_filtered.begin() + i); + } + } + unsupported_filtered = intersection_ex(last, + offset2_ex(unsupported_filtered, double(-perimeter_spacing / 2), double(bridged_infill_margin + perimeter_spacing / 2))); + if (this->config->counterbore_hole_bridging.value == chbFilled) { + for (ExPolygon& expol : unsupported_filtered) { + //check if the holes won't be covered by the upper layer + //TODO: if we want to do that, we must modify the geometry before making perimeters. + //if (this->upper_slices != nullptr && !this->upper_slices->expolygons.empty()) { + // for (Polygon &poly : expol.holes) poly.make_counter_clockwise(); + // float perimeterwidth = this->config->perimeters == 0 ? 0 : (this->ext_perimeter_flow.scaled_width() + (this->config->perimeters - 1) + this->perimeter_flow.scaled_spacing()); + // std::cout << "test upper slices with perimeterwidth=" << perimeterwidth << "=>" << offset_ex(this->upper_slices->expolygons, -perimeterwidth).size(); + // if (intersection(Polygons() = { expol.holes }, to_polygons(offset_ex(this->upper_slices->expolygons, -this->ext_perimeter_flow.scaled_width() / 2))).empty()) { + // std::cout << " EMPTY"; + // expol.holes.clear(); + // } else { + // } + // std::cout << "\n"; + //} else { + expol.holes.clear(); + //} - 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; + //detect inside volume + for (size_t surface_idx_other = 0; surface_idx_other < all_surfaces.size(); surface_idx_other++) { + if (surface_idx == surface_idx_other) continue; + if (intersection_ex(ExPolygons() = { expol }, ExPolygons() = { all_surfaces[surface_idx_other].expolygon }).size() > 0) { + //this means that other_surf was inside an expol holes + //as we removed them, we need to add a new one + ExPolygons new_poly = offset2_ex(ExPolygons{ all_surfaces[surface_idx_other].expolygon }, double(-bridged_infill_margin - perimeter_spacing), double(perimeter_spacing)); + if (new_poly.size() == 1) { + all_surfaces[surface_idx_other].expolygon = new_poly[0]; + expol.holes.push_back(new_poly[0].contour); + expol.holes.back().make_clockwise(); + } else { + for (size_t idx = 0; idx < new_poly.size(); idx++) { + Surface new_surf = all_surfaces[surface_idx_other]; + new_surf.expolygon = new_poly[idx]; + all_surfaces.push_back(new_surf); + expol.holes.push_back(new_poly[idx].contour); + expol.holes.back().make_clockwise(); + } + all_surfaces.erase(all_surfaces.begin() + surface_idx_other); + if (surface_idx_other < surface_idx) { + surface_idx--; + surface = &all_surfaces[surface_idx]; + } + surface_idx_other--; + } + } + } + } - 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); - } + } + //TODO: add other polys as holes inside this one (-margin) + } else if (/*this->config->counterbore_hole_bridging.value == chbBridgesOverhangs || */this->config->counterbore_hole_bridging.value == chbBridges) { + //simplify to avoid most of artefacts from printing lines. + ExPolygons bridgeable_simplified; + for (ExPolygon& poly : bridgeable) { + poly.simplify(perimeter_spacing, &bridgeable_simplified); + } + bridgeable_simplified = offset2_ex(bridgeable_simplified, -ext_perimeter_width, ext_perimeter_width); + //bridgeable_simplified = intersection_ex(bridgeable_simplified, unsupported_filtered); + //offset by perimeter spacing because the simplify may have reduced it a bit. + //it's not dangerous as it will be intersected by 'unsupported' later + //FIXME: add overlap in this->fill_surfaces->append + //FIXME: it overlap inside unsuppported not-bridgeable area! - 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; - }); + //bridgeable_simplified = offset2_ex(bridgeable_simplified, (double)-perimeter_spacing, (double)perimeter_spacing * 2); + //ExPolygons unbridgeable = offset_ex(diff_ex(unsupported, bridgeable_simplified), perimeter_spacing * 3 / 2); + //ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2))); + //unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width); - 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; - } + // if (this->config->counterbore_hole_bridging.value == chbBridges) { + ExPolygons unbridgeable = unsupported_filtered; + for (ExPolygon& expol : unbridgeable) + expol.holes.clear(); + unbridgeable = diff_ex(unbridgeable, bridgeable_simplified); + unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width * 2, ext_perimeter_width * 2); + ExPolygons bridges_temp = offset2_ex(intersection_ex(last, diff_ex(unsupported_filtered, unbridgeable), ApplySafetyOffset::Yes), -ext_perimeter_width / 4, ext_perimeter_width / 4); + //remove the overhangs section from the surface polygons + ExPolygons reference = last; + last = diff_ex(last, unsupported_filtered); + //ExPolygons no_bridge = diff_ex(offset_ex(unbridgeable, ext_perimeter_width * 3 / 2), last); + //bridges_temp = diff_ex(bridges_temp, no_bridge); + coordf_t offset_to_do = bridged_infill_margin; + bool first = true; + unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width)); + while (offset_to_do > ext_perimeter_width * 1.5) { + unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width / 4, ext_perimeter_width * 2.25, ClipperLib::jtSquare); + bridges_temp = diff_ex(bridges_temp, unbridgeable); + bridges_temp = offset_ex(bridges_temp, ext_perimeter_width, ClipperLib::jtMiter, 6.); + unbridgeable = diff_ex(unbridgeable, offset_ex(bridges_temp, ext_perimeter_width)); + offset_to_do -= ext_perimeter_width; + first = false; + } + unbridgeable = offset_ex(unbridgeable, ext_perimeter_width + offset_to_do, ClipperLib::jtSquare); + bridges_temp = diff_ex(bridges_temp, unbridgeable); + unsupported_filtered = offset_ex(bridges_temp, offset_to_do); + unsupported_filtered = intersection_ex(unsupported_filtered, reference); + // } else { + // ExPolygons unbridgeable = intersection_ex(unsupported, diff_ex(unsupported_filtered, offset_ex(bridgeable_simplified, ext_perimeter_width / 2))); + // unbridgeable = offset2_ex(unbridgeable, -ext_perimeter_width, ext_perimeter_width); + // unsupported_filtered = unbridgeable; - 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; + // ////put the bridge area inside the unsupported_filtered variable + // //unsupported_filtered = intersection_ex(last, + // // diff_ex( + // // offset_ex(bridgeable_simplified, (double)perimeter_spacing / 2), + // // unbridgeable + // // ) + // // ); + // } + } else { + unsupported_filtered.clear(); + } + } else { + unsupported_filtered.clear(); + } } - } - } - 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 (!unsupported_filtered.empty()) { - 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. - } - } + //add this directly to the infill list. + // this will avoid to throw wrong offsets into a good polygons + this->fill_surfaces->append( + unsupported_filtered, + stInternal); - // 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++; + // store the results + last = diff_ex(last, unsupported_filtered, ApplySafetyOffset::Yes); + //remove "thin air" polygons (note: it assumes that all polygons below will be extruded) + for (int i = 0; i < last.size(); i++) { + if (intersection_ex(support, ExPolygons() = { last[i] }).empty()) { + this->fill_surfaces->append( + ExPolygons() = { last[i] }, + stInternal); + last.erase(last.begin() + i); + i--; } } - - 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); + if (last.size() == 0) { + all_surfaces.erase(all_surfaces.begin() + surface_idx); + surface_idx--; + } else { + surface->expolygon = last[0]; + for (size_t idx = 1; idx < last.size(); idx++) { + all_surfaces.emplace_back(*surface, last[idx]); + } } - 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()); } } }