diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 56d98ec5af..d98cf7ba90 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -19,8 +19,14 @@ #include "Geometry/VoronoiVisualUtils.hpp" #include "../EdgeGrid.hpp" +#include +#include +#include + #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). +//#define ARACHNE_DEBUG + namespace boost::polygon { template<> struct geometry_concept @@ -285,7 +291,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 +391,42 @@ SkeletalTrapezoidation::SkeletalTrapezoidation(const Polygons& polys, const Bead constructFromPolygons(polys); } -bool detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments) { +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 Voronoi::VD::vertex_type &pt) +{ + return {pt.x(), pt.y()}; +} + +bool is_voronoi_diagram_planar(const Geometry::VoronoiDiagram &voronoi_diagram) { + assert(std::all_of(voronoi_diagram.edges().cbegin(), voronoi_diagram.edges().cend(), + [](const Geometry::VoronoiDiagram::edge_type &edge) { return edge.color() == 0; })); + + std::vector segments; + segments.reserve(voronoi_diagram.num_edges()); + + for (const Geometry::VoronoiDiagram::edge_type &edge : voronoi_diagram.edges()) { + if (edge.color() != 0) + continue; + + if (edge.is_finite() && edge.is_linear()) { + 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 Geometry::VoronoiDiagram::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 detect_missing_voronoi_vertex(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments) { for (VoronoiUtils::vd_t::cell_type cell : voronoi_diagram.cells()) { if (!cell.incident_edge()) continue; // There is no spoon @@ -432,6 +472,58 @@ 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_copy, + std::vector &segments, + const double fix_angle) +{ + std::unordered_map vertex_mapping; + for (Polygon &poly : polys_copy) + poly.rotate(fix_angle); + + 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]}); + } + + 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(is_voronoi_diagram_planar(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) { // Check self intersections. @@ -450,39 +542,49 @@ 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); +#ifdef ARACHNE_DEBUG + { + 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 + +#ifdef ARACHNE_DEBUG + assert(is_voronoi_diagram_planar(voronoi_diagram)); +#endif + // 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; 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); + BOOST_LOG_TRIVIAL(warning) << "Detected missing Voronoi vertex, input polygons will be rotated back and forth."; + vertex_mapping = try_to_fix_degenerated_voronoi_diagram_by_rotation(voronoi_diagram, polys, polys_copy, segments, fix_angle); - 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]}); - } - - 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)); if (detect_missing_voronoi_vertex(voronoi_diagram, segments)) BOOST_LOG_TRIVIAL(error) << "Detected missing Voronoi vertex even after the rotation of input."; } + bool degenerated_voronoi_diagram = has_missing_voronoi_vertex; + +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 +640,41 @@ 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. + // FIXME Lukas H.: Replace has_missing_twin_edge with detection if the Voronoi diagram is planar using a custom algorithm based on the + // sweeping line algorithm. At least it is required to fix GH #8446. + 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(is_voronoi_diagram_planar(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); + separatePointyQuadEndNodes(); graph.collapseSmallEdges(); diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index 51b24bbcdf..321ace9d73 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -18,6 +18,7 @@ #include "SkeletalTrapezoidationJoint.hpp" #include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp" #include "SkeletalTrapezoidationGraph.hpp" +#include "../Geometry/Voronoi.hpp" namespace Slic3r::Arachne { @@ -591,5 +592,7 @@ protected: void generateLocalMaximaSingleBeads(); }; +bool is_voronoi_diagram_planar(const Geometry::VoronoiDiagram &voronoi_diagram); + } // namespace Slic3r::Arachne #endif // VORONOI_QUADRILATERALIZATION_H diff --git a/tests/libslic3r/test_voronoi.cpp b/tests/libslic3r/test_voronoi.cpp index db12e2fec1..5d8dce146f 100644 --- a/tests/libslic3r/test_voronoi.cpp +++ b/tests/libslic3r/test_voronoi.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -2158,3 +2159,34 @@ TEST_CASE("Intersecting Voronoi edges", "[Voronoi]") // REQUIRE(!has_intersecting_edges(poly, vd)); } + +// In this case resulting Voronoi diagram is not planar. This case was distilled from GH issue #8474. +// Also, in GH issue #8514, a non-planar Voronoi diagram is generated for several polygons. +// Rotating the input polygon will solve this issue. +TEST_CASE("Non-planar voronoi diagram", "[VoronoiNonPlanar]") +{ + Polygon poly { + { 5500000, -42000000}, + { 8000000, -17000000}, + { 8000000, 40000000}, + { 7500000, 40000000}, + { 7500000, -18000000}, + { 6000001, -18000000}, + { 6000000, 40000000}, + { 5500000, 40000000}, + }; + +// poly.rotate(PI / 6); + + REQUIRE(poly.area() > 0.); + REQUIRE(intersecting_edges({poly}).empty()); + + VD vd; + Lines lines = to_lines(poly); + construct_voronoi(lines.begin(), lines.end(), &vd); +#ifdef VORONOI_DEBUG_OUT + dump_voronoi_to_svg(debug_out_path("voronoi-non-planar-out.svg").c_str(), vd, Points(), lines); +#endif + +// REQUIRE(Arachne::is_voronoi_diagram_planar(vd)); +}