diff --git a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp index 494b7b0b6..c8a84c401 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/DistributedBeadingStrategy.cpp @@ -21,7 +21,7 @@ DistributedBeadingStrategy::DistributedBeadingStrategy(const coord_t optimal_wid name = "DistributedBeadingStrategy"; } -DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const +DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(const coord_t thickness, const coord_t bead_count) const { Beading ret; @@ -40,18 +40,24 @@ DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(coord_t for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++) weights[bead_idx] = getWeight(bead_idx); - const float total_weight = std::accumulate(weights.cbegin(), weights.cend(), 0.f); + const float total_weight = std::accumulate(weights.cbegin(), weights.cend(), 0.f); + coord_t accumulated_width = 0; for (coord_t bead_idx = 0; bead_idx < bead_count; bead_idx++) { - const float weight_fraction = weights[bead_idx] / total_weight; + const float weight_fraction = weights[bead_idx] / total_weight; const coord_t splitup_left_over_weight = to_be_divided * weight_fraction; - const coord_t width = optimal_width + splitup_left_over_weight; + const coord_t width = (bead_idx == bead_count - 1) ? thickness - accumulated_width : optimal_width + splitup_left_over_weight; + + // Be aware that toolpath_locations is computed by dividing the width by 2, so toolpath_locations + // could be off by 1 because of rounding errors. if (bead_idx == 0) ret.toolpath_locations.emplace_back(width / 2); else ret.toolpath_locations.emplace_back(ret.toolpath_locations.back() + (ret.bead_widths.back() + width) / 2); ret.bead_widths.emplace_back(width); + accumulated_width += width; } ret.left_over = 0; + assert((accumulated_width + ret.left_over) == thickness); } else if (bead_count == 2) { const coord_t outer_width = thickness / 2; ret.bead_widths.emplace_back(outer_width); @@ -68,6 +74,13 @@ DistributedBeadingStrategy::Beading DistributedBeadingStrategy::compute(coord_t ret.left_over = thickness; } + assert(([&ret = std::as_const(ret), thickness]() -> bool { + coord_t total_bead_width = 0; + for (const coord_t &bead_width : ret.bead_widths) + total_bead_width += bead_width; + return (total_bead_width + ret.left_over) == thickness; + }())); + return ret; } diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 56d98ec5a..04dede546 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -17,6 +17,7 @@ #include "Utils.hpp" #include "SVG.hpp" #include "Geometry/VoronoiVisualUtils.hpp" +#include "Geometry/VoronoiUtilsCgal.hpp" #include "../EdgeGrid.hpp" #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). @@ -43,6 +44,71 @@ template<> struct segment_traits namespace Slic3r::Arachne { +#ifdef ARACHNE_DEBUG +static void export_graph_to_svg(const std::string &path, + SkeletalTrapezoidationGraph &graph, + const Polygons &polys, + const std::vector> &edge_junctions = {}, + const bool beat_count = true, + const bool transition_middles = true, + const bool transition_ends = true) +{ + const std::vector colors = {"blue", "cyan", "red", "orange", "magenta", "pink", "purple", "green", "yellow"}; + coordf_t stroke_width = scale_(0.03); + BoundingBox bbox = get_extents(polys); + for (const auto &node : graph.nodes) + bbox.merge(node.p); + + bbox.offset(scale_(1.)); + + ::Slic3r::SVG svg(path.c_str(), bbox); + for (const auto &line : to_lines(polys)) + svg.draw(line, "gray", stroke_width); + + for (const auto &edge : graph.edges) + svg.draw(Line(edge.from->p, edge.to->p), (edge.data.centralIsSet() && edge.data.isCentral()) ? "blue" : "cyan", stroke_width); + + for (const auto &line_junction : edge_junctions) + for (const auto &extrusion_junction : *line_junction) + svg.draw(extrusion_junction.p, "orange", coord_t(stroke_width * 2.)); + + if (beat_count) { + for (const auto &node : graph.nodes) { + svg.draw(node.p, "red", coord_t(stroke_width * 1.6)); + svg.draw_text(node.p, std::to_string(node.data.bead_count).c_str(), "black", 1); + } + } + + if (transition_middles) { + for (auto &edge : graph.edges) { + if (std::shared_ptr> transitions = edge.data.getTransitions(); transitions) { + for (auto &transition : *transitions) { + Line edge_line = Line(edge.to->p, edge.from->p); + double edge_length = edge_line.length(); + Point pt = edge_line.a + (edge_line.vector().cast() * (double(transition.pos) / edge_length)).cast(); + svg.draw(pt, "magenta", coord_t(stroke_width * 1.5)); + svg.draw_text(pt, std::to_string(transition.lower_bead_count).c_str(), "black", 1); + } + } + } + } + + if (transition_ends) { + for (auto &edge : graph.edges) { + if (std::shared_ptr> transitions = edge.data.getTransitionEnds(); transitions) { + for (auto &transition : *transitions) { + Line edge_line = Line(edge.to->p, edge.from->p); + double edge_length = edge_line.length(); + Point pt = edge_line.a + (edge_line.vector().cast() * (double(transition.pos) / edge_length)).cast(); + svg.draw(pt, transition.is_lower_end ? "green" : "lime", coord_t(stroke_width * 1.5)); + svg.draw_text(pt, std::to_string(transition.lower_bead_count).c_str(), "black", 1); + } + } + } + } +} +#endif + SkeletalTrapezoidation::node_t& SkeletalTrapezoidation::makeNode(vd_t::vertex_type& vd_node, Point p) { auto he_node_it = vd_node_to_he_node.find(&vd_node); @@ -285,7 +351,6 @@ std::vector SkeletalTrapezoidation::discretize(const vd_t::edge_type& vd_ } } - bool SkeletalTrapezoidation::computePointCellRange(vd_t::cell_type& cell, Point& start_source_point, Point& end_source_point, vd_t::edge_type*& starting_vd_edge, vd_t::edge_type*& ending_vd_edge, const std::vector& segments) { if (cell.incident_edge()->is_infinite()) @@ -386,7 +451,23 @@ SkeletalTrapezoidation::SkeletalTrapezoidation(const Polygons& polys, const Bead constructFromPolygons(polys); } -bool detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments) { +static bool has_finite_edge_with_non_finite_vertex(const Geometry::VoronoiDiagram &voronoi_diagram) +{ + for (const VoronoiUtils::vd_t::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 false; +} + +static bool detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments) { + if (has_finite_edge_with_non_finite_vertex(voronoi_diagram)) + return true; + for (VoronoiUtils::vd_t::cell_type cell : voronoi_diagram.cells()) { if (!cell.incident_edge()) continue; // There is no spoon @@ -405,7 +486,8 @@ bool detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagr VoronoiUtils::vd_t::edge_type *ending_vd_edge = nullptr; VoronoiUtils::vd_t::edge_type *edge = cell.incident_edge(); do { - if (edge->is_infinite()) continue; + if (edge->is_infinite() || edge->vertex0() == nullptr || edge->vertex1() == nullptr || !VoronoiUtils::is_finite(*edge->vertex0()) || !VoronoiUtils::is_finite(*edge->vertex1())) + continue; Vec2i64 v0 = VoronoiUtils::p(edge->vertex0()); Vec2i64 v1 = VoronoiUtils::p(edge->vertex1()); @@ -432,8 +514,71 @@ bool detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagr return false; } +static bool has_missing_twin_edge(const SkeletalTrapezoidationGraph &graph) +{ + for (const auto &edge : graph.edges) + if (edge.twin == nullptr) + return true; + return false; +} + +inline static std::unordered_map try_to_fix_degenerated_voronoi_diagram_by_rotation( + Geometry::VoronoiDiagram &voronoi_diagram, + const Polygons &polys, + Polygons &polys_rotated, + std::vector &segments, + const double fix_angle) +{ + std::unordered_map vertex_mapping; + for (Polygon &poly : polys_rotated) + poly.rotate(fix_angle); + + assert(polys_rotated.size() == polys.size()); + for (size_t poly_idx = 0; poly_idx < polys.size(); ++poly_idx) { + assert(polys_rotated[poly_idx].size() == polys[poly_idx].size()); + for (size_t point_idx = 0; point_idx < polys[poly_idx].size(); ++point_idx) + vertex_mapping.insert({polys_rotated[poly_idx][point_idx], polys[poly_idx][point_idx]}); + } + + segments.clear(); + for (size_t poly_idx = 0; poly_idx < polys_rotated.size(); poly_idx++) + for (size_t point_idx = 0; point_idx < polys_rotated[poly_idx].size(); point_idx++) + segments.emplace_back(&polys_rotated, poly_idx, point_idx); + + voronoi_diagram.clear(); + construct_voronoi(segments.begin(), segments.end(), &voronoi_diagram); + +#ifdef ARACHNE_DEBUG_VORONOI + { + static int iRun = 0; + dump_voronoi_to_svg(debug_out_path("arachne_voronoi-diagram-rotated-%d.svg", iRun++).c_str(), voronoi_diagram, to_points(polys), to_lines(polys)); + } +#endif + + assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(voronoi_diagram)); + + return vertex_mapping; +} + +inline static void rotate_back_skeletal_trapezoidation_graph_after_fix(SkeletalTrapezoidationGraph &graph, + const double fix_angle, + const std::unordered_map &vertex_mapping) +{ + for (STHalfEdgeNode &node : graph.nodes) { + // If a mapping exists between a rotated point and an original point, use this mapping. Otherwise, rotate a point in the opposite direction. + if (auto node_it = vertex_mapping.find(node.p); node_it != vertex_mapping.end()) + node.p = node_it->second; + else + node.p.rotate(-fix_angle); + } +} + void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) { +#ifdef ARACHNE_DEBUG + this->outline = polys; +#endif + // Check self intersections. assert([&polys]() -> bool { EdgeGrid::Grid grid; @@ -450,39 +595,57 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) for (size_t point_idx = 0; point_idx < polys[poly_idx].size(); point_idx++) segments.emplace_back(&polys, poly_idx, point_idx); +#ifdef ARACHNE_DEBUG + { + static int iRun = 0; + BoundingBox bbox = get_extents(polys); + SVG svg(debug_out_path("arachne_voronoi-input-%d.svg", iRun++).c_str(), bbox); + svg.draw_outline(polys, "black", scaled(0.03f)); + } +#endif + Geometry::VoronoiDiagram voronoi_diagram; construct_voronoi(segments.begin(), segments.end(), &voronoi_diagram); - // Try to detect cases when some Voronoi vertex is missing. - // When any Voronoi vertex is missing, rotate input polygon and try again. - const bool has_missing_voronoi_vertex = detect_missing_voronoi_vertex(voronoi_diagram, segments); - const double fix_angle = PI / 6; +#ifdef ARACHNE_DEBUG_VORONOI + { + static int iRun = 0; + dump_voronoi_to_svg(debug_out_path("arachne_voronoi-diagram-%d.svg", iRun++).c_str(), voronoi_diagram, to_points(polys), to_lines(polys)); + } +#endif + + // Try to detect cases when some Voronoi vertex is missing and when + // the Voronoi diagram is not planar. + // When any Voronoi vertex is missing, or the Voronoi diagram is not + // planar, rotate the input polygon and try again. + const bool has_missing_voronoi_vertex = detect_missing_voronoi_vertex(voronoi_diagram, segments); + // Detection of non-planar Voronoi diagram detects at least GH issues #8474, #8514 and #8446. + const bool is_voronoi_diagram_planar = Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram); + const double fix_angle = PI / 6; + std::unordered_map vertex_mapping; + // polys_copy is referenced through items stored in the std::vector segments. Polygons polys_copy = polys; - if (has_missing_voronoi_vertex) { - BOOST_LOG_TRIVIAL(debug) << "Detected missing Voronoi vertex, input polygons will be rotated back and forth."; - for (Polygon &poly : polys_copy) - poly.rotate(fix_angle); + if (has_missing_voronoi_vertex || !is_voronoi_diagram_planar) { + if (has_missing_voronoi_vertex) + BOOST_LOG_TRIVIAL(warning) << "Detected missing Voronoi vertex, input polygons will be rotated back and forth."; + else if (!is_voronoi_diagram_planar) + BOOST_LOG_TRIVIAL(warning) << "Detected non-planar Voronoi diagram, input polygons will be rotated back and forth."; - assert(polys_copy.size() == polys.size()); - for (size_t poly_idx = 0; poly_idx < polys.size(); ++poly_idx) { - assert(polys_copy[poly_idx].size() == polys[poly_idx].size()); - for (size_t point_idx = 0; point_idx < polys[poly_idx].size(); ++point_idx) - vertex_mapping.insert({polys[poly_idx][point_idx], polys_copy[poly_idx][point_idx]}); - } + vertex_mapping = try_to_fix_degenerated_voronoi_diagram_by_rotation(voronoi_diagram, polys, polys_copy, segments, fix_angle); - segments.clear(); - for (size_t poly_idx = 0; poly_idx < polys_copy.size(); poly_idx++) - for (size_t point_idx = 0; point_idx < polys_copy[poly_idx].size(); point_idx++) - segments.emplace_back(&polys_copy, poly_idx, point_idx); - - voronoi_diagram.clear(); - construct_voronoi(segments.begin(), segments.end(), &voronoi_diagram); assert(!detect_missing_voronoi_vertex(voronoi_diagram, segments)); + assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram)); if (detect_missing_voronoi_vertex(voronoi_diagram, segments)) BOOST_LOG_TRIVIAL(error) << "Detected missing Voronoi vertex even after the rotation of input."; + else if (!Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram)) + BOOST_LOG_TRIVIAL(error) << "Detected non-planar Voronoi diagram even after the rotation of input."; } + bool degenerated_voronoi_diagram = has_missing_voronoi_vertex || !is_voronoi_diagram_planar; + +process_voronoi_diagram: + assert(this->graph.edges.empty() && this->graph.nodes.empty() && this->vd_edge_to_he_edge.empty() && this->vd_node_to_he_node.empty()); for (vd_t::cell_type cell : voronoi_diagram.cells()) { if (!cell.incident_edge()) continue; // There is no spoon @@ -538,16 +701,43 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) prev_edge->to->data.distance_to_boundary = 0; } - if (has_missing_voronoi_vertex) { - for (node_t &node : graph.nodes) { - // If a mapping exists between a rotated point and an original point, use this mapping. Otherwise, rotate a point in the opposite direction. - if (auto node_it = vertex_mapping.find(node.p); node_it != vertex_mapping.end()) - node.p = node_it->second; - else - node.p.rotate(-fix_angle); - } + // For some input polygons, as in GH issues #8474 and #8514 resulting Voronoi diagram is degenerated because it is not planar. + // When this degenerated Voronoi diagram is processed, the resulting half-edge structure contains some edges that don't have + // a twin edge. Based on this, we created a fast mechanism that detects those causes and tries to recompute the Voronoi + // diagram on slightly rotated input polygons that usually make the Voronoi generator generate a non-degenerated Voronoi diagram. + if (!degenerated_voronoi_diagram && has_missing_twin_edge(this->graph)) { + BOOST_LOG_TRIVIAL(warning) << "Detected degenerated Voronoi diagram, input polygons will be rotated back and forth."; + degenerated_voronoi_diagram = true; + vertex_mapping = try_to_fix_degenerated_voronoi_diagram_by_rotation(voronoi_diagram, polys, polys_copy, segments, fix_angle); + + assert(!detect_missing_voronoi_vertex(voronoi_diagram, segments)); + if (detect_missing_voronoi_vertex(voronoi_diagram, segments)) + BOOST_LOG_TRIVIAL(error) << "Detected missing Voronoi vertex after the rotation of input."; + + assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(voronoi_diagram)); + + this->graph.edges.clear(); + this->graph.nodes.clear(); + this->vd_edge_to_he_edge.clear(); + this->vd_node_to_he_node.clear(); + + goto process_voronoi_diagram; } + if (degenerated_voronoi_diagram) { + assert(!has_missing_twin_edge(this->graph)); + + if (has_missing_twin_edge(this->graph)) + BOOST_LOG_TRIVIAL(error) << "Detected degenerated Voronoi diagram even after the rotation of input."; + } + + if (degenerated_voronoi_diagram) + rotate_back_skeletal_trapezoidation_graph_after_fix(this->graph, fix_angle, vertex_mapping); + +#ifdef ARACHNE_DEBUG + assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(voronoi_diagram)); +#endif + separatePointyQuadEndNodes(); graph.collapseSmallEdges(); @@ -594,45 +784,62 @@ void SkeletalTrapezoidation::separatePointyQuadEndNodes() // vvvvvvvvvvvvvvvvvvvvv // -#if 0 -static void export_graph_to_svg(const std::string &path, const SkeletalTrapezoidationGraph &graph, const Polygons &polys) -{ - const std::vector colors = {"blue", "cyan", "red", "orange", "magenta", "pink", "purple", "green", "yellow"}; - coordf_t stroke_width = scale_(0.05); - BoundingBox bbox; - for (const auto &node : graph.nodes) - bbox.merge(node.p); - - bbox.offset(scale_(1.)); - ::Slic3r::SVG svg(path.c_str(), bbox); - for (const auto &line : to_lines(polys)) - svg.draw(line, "red", stroke_width); - - for (const auto &edge : graph.edges) - svg.draw(Line(edge.from->p, edge.to->p), "cyan", scale_(0.01)); -} -#endif - void SkeletalTrapezoidation::generateToolpaths(std::vector &generated_toolpaths, bool filter_outermost_central_edges) { +#ifdef ARACHNE_DEBUG + static int iRun = 0; +#endif + p_generated_toolpaths = &generated_toolpaths; updateIsCentral(); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-updateIsCentral-final-%d.svg", iRun), this->graph, this->outline); +#endif + filterCentral(central_filter_dist); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-filterCentral-final-%d.svg", iRun), this->graph, this->outline); +#endif + if (filter_outermost_central_edges) filterOuterCentral(); updateBeadCount(); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-updateBeadCount-final-%d.svg", iRun), this->graph, this->outline); +#endif + filterNoncentralRegions(); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-filterNoncentralRegions-final-%d.svg", iRun), this->graph, this->outline); +#endif + generateTransitioningRibs(); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-final-%d.svg", iRun), this->graph, this->outline); +#endif + generateExtraRibs(); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateExtraRibs-final-%d.svg", iRun), this->graph, this->outline); +#endif + generateSegments(); + +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateSegments-final-%d.svg", iRun), this->graph, this->outline); +#endif + +#ifdef ARACHNE_DEBUG + ++iRun; +#endif } void SkeletalTrapezoidation::updateIsCentral() @@ -844,11 +1051,24 @@ void SkeletalTrapezoidation::generateTransitioningRibs() filterTransitionMids(); +#ifdef ARACHNE_DEBUG + static int iRun = 0; + export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-mids-%d.svg", iRun++), this->graph, this->outline); +#endif + ptr_vector_t> edge_transition_ends; // We only map the half edge in the upward direction. mapped items are not sorted generateAllTransitionEnds(edge_transition_ends); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateTransitioningRibs-ends-%d.svg", iRun++), this->graph, this->outline); +#endif + applyTransitions(edge_transition_ends); // Note that the shared pointer lists will be out of scope and thus destroyed here, since the remaining refs are weak_ptr. + +#ifdef ARACHNE_DEBUG + ++iRun; +#endif } @@ -1568,17 +1788,38 @@ void SkeletalTrapezoidation::generateSegments() } } } - + +#ifdef ARACHNE_DEBUG + static int iRun = 0; + export_graph_to_svg(debug_out_path("ST-generateSegments-before-propagation-%d.svg", iRun), this->graph, this->outline); +#endif + propagateBeadingsUpward(upward_quad_mids, node_beadings); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateSegments-upward-propagation-%d.svg", iRun), this->graph, this->outline); +#endif + propagateBeadingsDownward(upward_quad_mids, node_beadings); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateSegments-downward-propagation-%d.svg", iRun), this->graph, this->outline); +#endif + ptr_vector_t edge_junctions; // junctions ordered high R to low R generateJunctions(node_beadings, edge_junctions); +#ifdef ARACHNE_DEBUG + export_graph_to_svg(debug_out_path("ST-generateSegments-junctions-%d.svg", iRun), this->graph, this->outline, edge_junctions); +#endif + connectJunctions(edge_junctions); - + generateLocalMaximaSingleBeads(); + +#ifdef ARACHNE_DEBUG + ++iRun; +#endif } SkeletalTrapezoidation::edge_t* SkeletalTrapezoidation::getQuadMaxRedgeTo(edge_t* quad_start_edge) @@ -1811,7 +2052,10 @@ void SkeletalTrapezoidation::generateJunctions(ptr_vector_t& for (junction_idx = (std::max(size_t(1), beading->toolpath_locations.size()) - 1) / 2; junction_idx < num_junctions; junction_idx--) { coord_t bead_R = beading->toolpath_locations[junction_idx]; - if (bead_R <= start_R) + // toolpath_locations computed inside DistributedBeadingStrategy could be off by 1 because of rounding errors. + // In GH issue #8472, these roundings errors caused missing the middle extrusion. + // Adding small epsilon should help resolve those cases. + if (bead_R <= start_R + 1) { // Junction coinciding with start node is used in this function call break; } diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index 51b24bbcd..819b71367 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -18,6 +18,10 @@ #include "SkeletalTrapezoidationJoint.hpp" #include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" #include "SkeletalTrapezoidationGraph.hpp" +#include "../Geometry/Voronoi.hpp" + +//#define ARACHNE_DEBUG +//#define ARACHNE_DEBUG_VORONOI namespace Slic3r::Arachne { @@ -122,6 +126,10 @@ public: */ void generateToolpaths(std::vector &generated_toolpaths, bool filter_outermost_central_edges = false); +#ifdef ARACHNE_DEBUG + Polygons outline; +#endif + protected: /*! * Auxiliary for referencing one transition along an edge which may contain multiple transitions diff --git a/src/libslic3r/Arachne/WallToolPaths.cpp b/src/libslic3r/Arachne/WallToolPaths.cpp index d40b6c488..13bc90172 100644 --- a/src/libslic3r/Arachne/WallToolPaths.cpp +++ b/src/libslic3r/Arachne/WallToolPaths.cpp @@ -24,18 +24,19 @@ namespace Slic3r::Arachne { WallToolPaths::WallToolPaths(const Polygons& outline, const coord_t bead_width_0, const coord_t bead_width_x, - const size_t inset_count, const coord_t wall_0_inset, const WallToolPathsParams ¶ms) + const size_t inset_count, const coord_t wall_0_inset, const coordf_t layer_height, const WallToolPathsParams ¶ms) : outline(outline) , bead_width_0(bead_width_0) , bead_width_x(bead_width_x) , inset_count(inset_count) , wall_0_inset(wall_0_inset) + , layer_height(layer_height) , print_thin_walls(Slic3r::Arachne::fill_outline_gaps) , min_feature_size(scaled(params.min_feature_size)) , min_bead_width(scaled(params.min_bead_width)) , small_area_length(static_cast(bead_width_0) / 2.) - , toolpaths_generated(false) , wall_transition_filter_deviation(scaled(params.wall_transition_filter_deviation)) + , toolpaths_generated(false) , m_params(params) { } @@ -312,60 +313,46 @@ void removeSmallAreas(Polygons &thiss, const double min_area_size, const bool re }; auto new_end = thiss.end(); - if(remove_holes) - { - for(auto it = thiss.begin(); it < new_end; it++) - { - // All polygons smaller than target are removed by replacing them with a polygon from the back of the vector - if(fabs(ClipperLib::Area(to_path(*it))) < min_area_size) - { - new_end--; + if (remove_holes) { + for (auto it = thiss.begin(); it < new_end;) { + // All polygons smaller than target are removed by replacing them with a polygon from the back of the vector. + if (fabs(ClipperLib::Area(to_path(*it))) < min_area_size) { + --new_end; *it = std::move(*new_end); - it--; // wind back the iterator such that the polygon just swaped in is checked next + continue; // Don't increment the iterator such that the polygon just swapped in is checked next. } + ++it; } - } - else - { + } else { // For each polygon, computes the signed area, move small outlines at the end of the vector and keep pointer on small holes std::vector small_holes; - for(auto it = thiss.begin(); it < new_end; it++) { - double area = ClipperLib::Area(to_path(*it)); - if (fabs(area) < min_area_size) - { - if(area >= 0) - { - new_end--; - if(it < new_end) { + for (auto it = thiss.begin(); it < new_end;) { + if (double area = ClipperLib::Area(to_path(*it)); fabs(area) < min_area_size) { + if (area >= 0) { + --new_end; + if (it < new_end) { std::swap(*new_end, *it); - it--; - } - else - { // Don't self-swap the last Path + continue; + } else { // Don't self-swap the last Path break; } - } - else - { + } else { small_holes.push_back(*it); } } + ++it; } // Removes small holes that have their first point inside one of the removed outlines // Iterating in reverse ensures that unprocessed small holes won't be moved const auto removed_outlines_start = new_end; - for(auto hole_it = small_holes.rbegin(); hole_it < small_holes.rend(); hole_it++) - { - for(auto outline_it = removed_outlines_start; outline_it < thiss.end() ; outline_it++) - { - if(Polygon(*outline_it).contains(*hole_it->begin())) { + for (auto hole_it = small_holes.rbegin(); hole_it < small_holes.rend(); hole_it++) + for (auto outline_it = removed_outlines_start; outline_it < thiss.end(); outline_it++) + if (Polygon(*outline_it).contains(*hole_it->begin())) { new_end--; *hole_it = std::move(*new_end); break; } - } - } } thiss.resize(new_end-thiss.begin()); } @@ -471,7 +458,7 @@ const std::vector &WallToolPaths::generate() // The functions above could produce intersecting polygons that could cause a crash inside Arachne. // Applying Clipper union should be enough to get rid of this issue. // Clipper union also fixed an issue in Arachne that in post-processing Voronoi diagram, some edges - // didn't have twin edges (this probably isn't an issue in Boost Voronoi generator). + // didn't have twin edges. (a non-planar Voronoi diagram probably caused this). prepared_outline = union_(prepared_outline); if (area(prepared_outline) <= 0) { @@ -479,9 +466,14 @@ const std::vector &WallToolPaths::generate() return toolpaths; } + const float external_perimeter_extrusion_width = Flow::rounded_rectangle_extrusion_width_from_spacing(unscale(bead_width_0), float(this->layer_height)); + const float perimeter_extrusion_width = Flow::rounded_rectangle_extrusion_width_from_spacing(unscale(bead_width_x), float(this->layer_height)); + const coord_t wall_transition_length = scaled(this->m_params.wall_transition_length); - const double wall_split_middle_threshold = this->m_params.wall_split_middle_threshold; // For an uneven nr. of lines: When to split the middle wall into two. - const double wall_add_middle_threshold = this->m_params.wall_add_middle_threshold; // For an even nr. of lines: When to add a new middle in between the innermost two walls. + + const double wall_split_middle_threshold = std::clamp(2. * unscaled(this->min_bead_width) / external_perimeter_extrusion_width - 1., 0.01, 0.99); // For an uneven nr. of lines: When to split the middle wall into two. + const double wall_add_middle_threshold = std::clamp(unscaled(this->min_bead_width) / perimeter_extrusion_width, 0.01, 0.99); // For an even nr. of lines: When to add a new middle in between the innermost two walls. + const int wall_distribution_count = this->m_params.wall_distribution_count; const size_t max_bead_count = (inset_count < std::numeric_limits::max() / 2) ? 2 * inset_count : std::numeric_limits::max(); const auto beading_strat = BeadingStrategyFactory::makeStrategy @@ -609,6 +601,14 @@ void WallToolPaths::stitchToolPaths(std::vector &toolpaths, { continue; } + + // PolylineStitcher, in some cases, produced closed extrusion (polygons), + // but the endpoints differ by a small distance. So we reconnect them. + // FIXME Lukas H.: Investigate more deeply why it is happening. + if (wall_polygon.junctions.front().p != wall_polygon.junctions.back().p && + (wall_polygon.junctions.back().p - wall_polygon.junctions.front().p).cast().norm() < stitch_distance) { + wall_polygon.junctions.emplace_back(wall_polygon.junctions.front()); + } wall_polygon.is_closed = true; wall_lines.emplace_back(std::move(wall_polygon)); // add stitched polygons to result } diff --git a/src/libslic3r/Arachne/WallToolPaths.hpp b/src/libslic3r/Arachne/WallToolPaths.hpp index 2f9879f0c..6bb115319 100644 --- a/src/libslic3r/Arachne/WallToolPaths.hpp +++ b/src/libslic3r/Arachne/WallToolPaths.hpp @@ -29,8 +29,6 @@ public: float wall_transition_angle; float wall_transition_filter_deviation; int wall_distribution_count; - float wall_add_middle_threshold; - float wall_split_middle_threshold; }; class WallToolPaths @@ -44,7 +42,7 @@ public: * \param inset_count The maximum number of parallel extrusion lines that make up the wall * \param wall_0_inset How far to inset the outer wall, to make it adhere better to other walls. */ - WallToolPaths(const Polygons& outline, coord_t bead_width_0, coord_t bead_width_x, size_t inset_count, coord_t wall_0_inset, const WallToolPathsParams ¶ms); + WallToolPaths(const Polygons& outline, coord_t bead_width_0, coord_t bead_width_x, size_t inset_count, coord_t wall_0_inset, coordf_t layer_height, const WallToolPathsParams ¶ms); /*! * Generates the Toolpaths @@ -123,14 +121,15 @@ private: coord_t bead_width_x; // toolpaths; //x(); const double y = node->y(); + assert(std::isfinite(x) && std::isfinite(y)); assert(x <= double(std::numeric_limits::max()) && x >= std::numeric_limits::lowest()); assert(y <= double(std::numeric_limits::max()) && y >= std::numeric_limits::lowest()); return {int64_t(x + 0.5 - (x < 0)), int64_t(y + 0.5 - (y < 0))}; // Round to the nearest integer coordinates. diff --git a/src/libslic3r/Arachne/utils/VoronoiUtils.hpp b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp index e736f98bc..aa4693643 100644 --- a/src/libslic3r/Arachne/utils/VoronoiUtils.hpp +++ b/src/libslic3r/Arachne/utils/VoronoiUtils.hpp @@ -35,6 +35,11 @@ public: * The \p approximate_step_size is measured parallel to the \p source_segment, not along the parabola. */ static std::vector discretizeParabola(const Point &source_point, const Segment &source_segment, Point start, Point end, coord_t approximate_step_size, float transitioning_angle); + + static inline bool is_finite(const VoronoiUtils::vd_t::vertex_type &vertex) + { + return std::isfinite(vertex.x()) && std::isfinite(vertex.y()); + } }; } // namespace Slic3r::Arachne diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 6203a0725..66874674a 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -151,6 +151,8 @@ set(lisbslic3r_sources Geometry/Voronoi.hpp Geometry/VoronoiOffset.cpp Geometry/VoronoiOffset.hpp + Geometry/VoronoiUtilsCgal.cpp + Geometry/VoronoiUtilsCgal.hpp Geometry/VoronoiVisualUtils.hpp Int128.hpp InternalBridgeDetector.cpp diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 3d2be9cf3..0f1d8f8e2 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -441,6 +441,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.anchor_length_max = surface_fill.params.anchor_length_max; params.resolution = resolution; params.use_arachne = surface_fill.params.pattern == ipConcentric; + params.layer_height = m_regions[surface_fill.region_id]->layer()->height; // BBS params.flow = surface_fill.params.flow; diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 74a3f630e..9487e463b 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -65,6 +65,8 @@ struct FillParams // For Concentric infill, to switch between Classic and Arachne. bool use_arachne{ false }; + // Layer height for Concentric infill with Arachne. + coordf_t layer_height { 0.f }; // BBS Flow flow; diff --git a/src/libslic3r/Fill/FillConcentric.cpp b/src/libslic3r/Fill/FillConcentric.cpp index fe9b79e09..3bcfc23b4 100644 --- a/src/libslic3r/Fill/FillConcentric.cpp +++ b/src/libslic3r/Fill/FillConcentric.cpp @@ -83,15 +83,13 @@ void FillConcentric::_fill_surface_single(const FillParams& params, double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end()); Arachne::WallToolPathsParams input_params; input_params.min_bead_width = 0.85 * min_nozzle_diameter; - input_params.min_feature_size = 0.1; + input_params.min_feature_size = 0.25 * min_nozzle_diameter; input_params.wall_transition_length = 1.0 * min_nozzle_diameter; input_params.wall_transition_angle = 10; input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter; input_params.wall_distribution_count = 1; - input_params.wall_add_middle_threshold = 0.75; - input_params.wall_split_middle_threshold = 0.5; - Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, input_params); + Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params); std::vector loops = wallToolPaths.getToolPaths(); std::vector all_extrusions; diff --git a/src/libslic3r/Fill/FillConcentricInternal.cpp b/src/libslic3r/Fill/FillConcentricInternal.cpp index 33878578c..8b3e90930 100644 --- a/src/libslic3r/Fill/FillConcentricInternal.cpp +++ b/src/libslic3r/Fill/FillConcentricInternal.cpp @@ -27,15 +27,13 @@ void FillConcentricInternal::fill_surface_extrusion(const Surface* surface, cons double min_nozzle_diameter = *std::min_element(print_config->nozzle_diameter.values.begin(), print_config->nozzle_diameter.values.end()); Arachne::WallToolPathsParams input_params; input_params.min_bead_width = 0.85 * min_nozzle_diameter; - input_params.min_feature_size = 0.1; + input_params.min_feature_size = 0.25 * min_nozzle_diameter; input_params.wall_transition_length = 0.4; input_params.wall_transition_angle = 10; input_params.wall_transition_filter_deviation = 0.25 * min_nozzle_diameter; input_params.wall_distribution_count = 1; - input_params.wall_add_middle_threshold = 0.75; - input_params.wall_split_middle_threshold = 0.5; - Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, input_params); + Arachne::WallToolPaths wallToolPaths(polygons, min_spacing, min_spacing, loops_count, 0, params.layer_height, input_params); std::vector loops = wallToolPaths.getToolPaths(); std::vector all_extrusions; diff --git a/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp new file mode 100644 index 000000000..062a3b397 --- /dev/null +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp @@ -0,0 +1,103 @@ +#include +#include +#include + +#include "libslic3r/Geometry/Voronoi.hpp" +#include "libslic3r/Arachne/utils/VoronoiUtils.hpp" + +#include "VoronoiUtilsCgal.hpp" + +using VD = Slic3r::Geometry::VoronoiDiagram; + +namespace Slic3r::Geometry { + +using CGAL_Point = CGAL::Exact_predicates_exact_constructions_kernel::Point_2; +using CGAL_Segment = CGAL::Arr_segment_traits_2::Curve_2; + +inline static CGAL_Point to_cgal_point(const VD::vertex_type &pt) { return {pt.x(), pt.y()}; } + +// FIXME Lukas H.: Also includes parabolic segments. +bool VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(const VD &voronoi_diagram) +{ + assert(std::all_of(voronoi_diagram.edges().cbegin(), voronoi_diagram.edges().cend(), + [](const VD::edge_type &edge) { return edge.color() == 0; })); + + std::vector segments; + segments.reserve(voronoi_diagram.num_edges()); + + for (const VD::edge_type &edge : voronoi_diagram.edges()) { + if (edge.color() != 0) + continue; + + if (edge.is_finite() && edge.is_linear() && edge.vertex0() != nullptr && edge.vertex1() != nullptr && + Arachne::VoronoiUtils::is_finite(*edge.vertex0()) && Arachne::VoronoiUtils::is_finite(*edge.vertex1())) { + segments.emplace_back(to_cgal_point(*edge.vertex0()), to_cgal_point(*edge.vertex1())); + edge.color(1); + assert(edge.twin() != nullptr); + edge.twin()->color(1); + } + } + + for (const VD::edge_type &edge : voronoi_diagram.edges()) + edge.color(0); + + std::vector intersections_pt; + CGAL::compute_intersection_points(segments.begin(), segments.end(), std::back_inserter(intersections_pt)); + return intersections_pt.empty(); +} + +static bool check_if_three_vectors_are_ccw(const CGAL_Point &common_pt, const CGAL_Point &pt_1, const CGAL_Point &pt_2, const CGAL_Point &test_pt) { + CGAL::Orientation orientation = CGAL::orientation(common_pt, pt_1, pt_2); + if (orientation == CGAL::Orientation::COLLINEAR) { + // The first two edges are collinear, so the third edge must be on the right side on the first of them. + return CGAL::orientation(common_pt, pt_1, test_pt) == CGAL::Orientation::RIGHT_TURN; + } else if (orientation == CGAL::Orientation::LEFT_TURN) { + // CCW oriented angle between vectors (common_pt, pt1) and (common_pt, pt2) is bellow PI. + // So we need to check if test_pt isn't between them. + CGAL::Orientation orientation1 = CGAL::orientation(common_pt, pt_1, test_pt); + CGAL::Orientation orientation2 = CGAL::orientation(common_pt, pt_2, test_pt); + return (orientation1 != CGAL::Orientation::LEFT_TURN || orientation2 != CGAL::Orientation::RIGHT_TURN); + } else { + assert(orientation == CGAL::Orientation::RIGHT_TURN); + // CCW oriented angle between vectors (common_pt, pt1) and (common_pt, pt2) is upper PI. + // So we need to check if test_pt is between them. + CGAL::Orientation orientation1 = CGAL::orientation(common_pt, pt_1, test_pt); + CGAL::Orientation orientation2 = CGAL::orientation(common_pt, pt_2, test_pt); + return (orientation1 == CGAL::Orientation::RIGHT_TURN || orientation2 == CGAL::Orientation::LEFT_TURN); + } +} + +bool VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(const VoronoiDiagram &voronoi_diagram) +{ + for (const VD::vertex_type &vertex : voronoi_diagram.vertices()) { + std::vector edges; + const VD::edge_type *edge = vertex.incident_edge(); + + do { + // FIXME Lukas H.: Also process parabolic segments. + if (edge->is_finite() && edge->is_linear() && edge->vertex0() != nullptr && edge->vertex1() != nullptr && + Arachne::VoronoiUtils::is_finite(*edge->vertex0()) && Arachne::VoronoiUtils::is_finite(*edge->vertex1())) + edges.emplace_back(edge); + + edge = edge->rot_next(); + } while (edge != vertex.incident_edge()); + + // Checking for CCW make sense for three and more edges. + if (edges.size() > 2) { + for (auto edge_it = edges.begin() ; edge_it != edges.end(); ++edge_it) { + const Geometry::VoronoiDiagram::edge_type *prev_edge = edge_it == edges.begin() ? edges.back() : *std::prev(edge_it); + const Geometry::VoronoiDiagram::edge_type *curr_edge = *edge_it; + const Geometry::VoronoiDiagram::edge_type *next_edge = std::next(edge_it) == edges.end() ? edges.front() : *std::next(edge_it); + + if (!check_if_three_vectors_are_ccw(to_cgal_point(*prev_edge->vertex0()), to_cgal_point(*prev_edge->vertex1()), + to_cgal_point(*curr_edge->vertex1()), to_cgal_point(*next_edge->vertex1()))) + return false; + } + } + } + + return true; +} + + +} // namespace Slic3r::Geometry \ No newline at end of file diff --git a/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp b/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp new file mode 100644 index 000000000..897891bd9 --- /dev/null +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp @@ -0,0 +1,21 @@ +#ifndef slic3r_VoronoiUtilsCgal_hpp_ +#define slic3r_VoronoiUtilsCgal_hpp_ + +#include "Voronoi.hpp" + +namespace Slic3r::Geometry { +class VoronoiDiagram; + +class VoronoiUtilsCgal +{ +public: + // Check if the Voronoi diagram is planar using CGAL sweeping edge algorithm for enumerating all intersections between lines. + static bool is_voronoi_diagram_planar_intersection(const VoronoiDiagram &voronoi_diagram); + + // Check if the Voronoi diagram is planar using verification that all neighboring edges are ordered CCW for each vertex. + static bool is_voronoi_diagram_planar_angle(const VoronoiDiagram &voronoi_diagram); + +}; +} // namespace Slic3r::Geometry + +#endif // slic3r_VoronoiUtilsCgal_hpp_ diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 03e42d392..bff4318d7 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -753,10 +753,19 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "bottom_surface_pattern" || opt_key == "external_fill_link_max_length" || opt_key == "infill_direction" - || opt_key == "sparse_infill_pattern" || opt_key == "top_surface_line_width" || opt_key == "initial_layer_line_width") { steps.emplace_back(posInfill); + } else if (opt_key == "sparse_infill_pattern") { + steps.emplace_back(posInfill); + + const auto *old_fill_pattern = old_config.option>(opt_key); + const auto *new_fill_pattern = new_config.option>(opt_key); + assert(old_infill && new_infill); + // We need to recalculate infill surfaces when infill_only_where_needed is enabled, and we are switching from + // the Lightning infill to another infill or vice versa. + if (PrintObject::infill_only_where_needed && (new_fill_pattern->value == ipLightning || old_fill_pattern->value == ipLightning)) + steps.emplace_back(posPrepareInfill); } else if (opt_key == "sparse_infill_density") { // One likely wants to reslice only when switching between zero infill to simulate boolean difference (subtracting volumes), // normal infill and 100% (solid) infill.