From 424ef02d8aaedf540868c133c4b19f913bb959fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Tue, 13 Sep 2022 20:38:15 +0200 Subject: [PATCH 1/5] Added a test case for a missing part of a model cased by WallToolPaths::simplifyToolPaths that was reported in #8849. --- tests/libslic3r/test_arachne.cpp | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/libslic3r/test_arachne.cpp b/tests/libslic3r/test_arachne.cpp index 2334fbc62..af48c71a1 100644 --- a/tests/libslic3r/test_arachne.cpp +++ b/tests/libslic3r/test_arachne.cpp @@ -451,3 +451,38 @@ TEST_CASE("Arachne - Missing infill", "[ArachneMissingInfill]") { // REQUIRE(wallToolPaths.getInnerContour().size() == 1); } + +// This test case was distilled from GitHub issue #8849. +// Missing part of the model after simplifying generated tool-paths by simplifyToolPaths. +TEST_CASE("Arachne - #8849 - Missing part of model", "[ArachneMissingPart8849]") { + const Polygon poly_0 = { + Point(-29700000, -10600000), + Point(-28200000, -10600000), + Point( 20000000, -10600000), + Point( 20000000, - 9900000), + Point(-28200000, - 9900000), + Point(-28200000, 0), + Point(-29700000, 0), + }; + + Polygons polygons = {poly_0}; + coord_t ext_perimeter_spacing = 449999; + coord_t perimeter_spacing = 757079; + coord_t inset_count = 2; + + Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.32, PrintObjectConfig::defaults(), PrintConfig::defaults()); + wall_tool_paths.generate(); + std::vector perimeters = wall_tool_paths.getToolPaths(); + +#ifdef ARACHNE_DEBUG_OUT + export_perimeters_to_svg(debug_out_path("arachne-missing-part-8849.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); +#endif + + int64_t total_extrusion_length= 0; + for (Arachne::VariableWidthLines &perimeter : perimeters) + for (Arachne::ExtrusionLine &extrusion_line : perimeter) + total_extrusion_length += extrusion_line.getLength(); + + // Total extrusion length should be around 30mm when the part is missing and around 120 when everything is ok. + // REQUIRE(total_extrusion_length >= scaled(120.)); +} From b2b9444b6e03d62d97d905dba27189fb9bd6df43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Thu, 15 Sep 2022 08:59:11 +0200 Subject: [PATCH 2/5] Fix of #8827: Parsing error in the pressure equalizer when tag TIMELAPSE_TAKE_FRAME was inside G-code. --- src/libslic3r/GCode/PressureEqualizer.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/GCode/PressureEqualizer.cpp b/src/libslic3r/GCode/PressureEqualizer.cpp index c8a9618ef..399ab4272 100644 --- a/src/libslic3r/GCode/PressureEqualizer.cpp +++ b/src/libslic3r/GCode/PressureEqualizer.cpp @@ -367,7 +367,16 @@ bool PressureEqualizer::process_line(const char *line, const char *line_end, GCo case 'T': { // Activate an extruder head. - int new_extruder = parse_int(line); + int new_extruder = -1; + try { + new_extruder = parse_int(line); + } catch (Slic3r::InvalidArgument &) { + // Ignore invalid GCodes starting with T. + eatws(line); + break; + } + assert(new_extruder != -1); + if (new_extruder != int(m_current_extruder)) { m_current_extruder = new_extruder; m_retracted = true; From 8ac60ccc7ac245adb87356bf9fbef5cae720c563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Sat, 17 Sep 2022 13:21:10 +0200 Subject: [PATCH 3/5] Extend the algorithm for detecting non-planar Voronoi diagrams to include testing orientation between a line and a parabola and testing orientation between two parabolas. It fixed most of the issues reported in #8846. Co-authored-by: Vojtech Bubnik --- .../Arachne/SkeletalTrapezoidation.cpp | 8 +- src/libslic3r/Geometry/VoronoiUtilsCgal.cpp | 216 ++++++++++++++++-- src/libslic3r/Geometry/VoronoiUtilsCgal.hpp | 3 +- 3 files changed, 204 insertions(+), 23 deletions(-) diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 04a1042d8..34caee311 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -619,8 +619,8 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) // 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); + // Detection of non-planar Voronoi diagram detects at least GH issues #8474, #8514, #8446 and #8846. + const bool is_voronoi_diagram_planar = Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments); const double fix_angle = PI / 6; std::unordered_map vertex_mapping; @@ -635,10 +635,10 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) 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)); - assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram)); + assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(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."; - else if (!Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram)) + else if (!Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments)) BOOST_LOG_TRIVIAL(error) << "Detected non-planar Voronoi diagram even after the rotation of input."; } diff --git a/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp index 062a3b397..c3348110b 100644 --- a/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.cpp @@ -8,17 +8,135 @@ #include "VoronoiUtilsCgal.hpp" using VD = Slic3r::Geometry::VoronoiDiagram; +using namespace Slic3r::Arachne; namespace Slic3r::Geometry { -using CGAL_Point = CGAL::Exact_predicates_exact_constructions_kernel::Point_2; -using CGAL_Segment = CGAL::Arr_segment_traits_2::Curve_2; +// The tangent vector of the parabola is computed based on the Proof of the reflective property. +// https://en.wikipedia.org/wiki/Parabola#Proof_of_the_reflective_property +// https://math.stackexchange.com/q/2439647/2439663#comment5039739_2439663 +namespace impl { + using K = CGAL::Simple_cartesian; + using FK = CGAL::Simple_cartesian; + using EK = CGAL::Simple_cartesian; + using C2E = CGAL::Cartesian_converter; + using C2F = CGAL::Cartesian_converter; + class Epick : public CGAL::Filtered_kernel_adaptor::Type, Epick>, true> {}; -inline static CGAL_Point to_cgal_point(const VD::vertex_type &pt) { return {pt.x(), pt.y()}; } + template + inline typename K::Vector_2 calculate_parabolic_tangent_vector( + // Test point on the parabola, where the tangent will be calculated. + const typename K::Point_2 &p, + // Focus point of the parabola. + const typename K::Point_2 &f, + // Points of a directrix of the parabola. + const typename K::Point_2 &u, + const typename K::Point_2 &v, + // On which side of the parabolic segment endpoints the focus point is, which determines the orientation of the tangent. + const typename K::Orientation &tangent_orientation) + { + using RT = typename K::RT; + using Vector_2 = typename K::Vector_2; + + const Vector_2 directrix_vec = v - u; + const RT directrix_vec_sqr_length = CGAL::scalar_product(directrix_vec, directrix_vec); + Vector_2 focus_vec = (f - u) * directrix_vec_sqr_length - directrix_vec * CGAL::scalar_product(directrix_vec, p - u); + Vector_2 tangent_vec = focus_vec.perpendicular(tangent_orientation); + return tangent_vec; + } + + template struct ParabolicTangentToSegmentOrientationPredicate + { + using Point_2 = typename K::Point_2; + using Vector_2 = typename K::Vector_2; + using Orientation = typename K::Orientation; + using result_type = typename K::Orientation; + + result_type operator()( + // Test point on the parabola, where the tangent will be calculated. + const Point_2 &p, + // End of the linear segment (p, q), for which orientation towards the tangent to parabola will be evaluated. + const Point_2 &q, + // Focus point of the parabola. + const Point_2 &f, + // Points of a directrix of the parabola. + const Point_2 &u, + const Point_2 &v, + // On which side of the parabolic segment endpoints the focus point is, which determines the orientation of the tangent. + const Orientation &tangent_orientation) const + { + assert(tangent_orientation == CGAL::Orientation::LEFT_TURN || tangent_orientation == CGAL::Orientation::RIGHT_TURN); + + Vector_2 tangent_vec = calculate_parabolic_tangent_vector(p, f, u, v, tangent_orientation); + Vector_2 linear_vec = q - p; + + return CGAL::sign(tangent_vec.x() * linear_vec.y() - tangent_vec.y() * linear_vec.x()); + } + }; + + template struct ParabolicTangentToParabolicTangentOrientationPredicate + { + using Point_2 = typename K::Point_2; + using Vector_2 = typename K::Vector_2; + using Orientation = typename K::Orientation; + using result_type = typename K::Orientation; + + result_type operator()( + // Common point on both parabolas, where the tangent will be calculated. + const Point_2 &p, + // Focus point of the first parabola. + const Point_2 &f_0, + // Points of a directrix of the first parabola. + const Point_2 &u_0, + const Point_2 &v_0, + // On which side of the parabolic segment endpoints the focus point is, which determines the orientation of the tangent. + const Orientation &tangent_orientation_0, + // Focus point of the second parabola. + const Point_2 &f_1, + // Points of a directrix of the second parabola. + const Point_2 &u_1, + const Point_2 &v_1, + // On which side of the parabolic segment endpoints the focus point is, which determines the orientation of the tangent. + const Orientation &tangent_orientation_1) const + { + assert(tangent_orientation_0 == CGAL::Orientation::LEFT_TURN || tangent_orientation_0 == CGAL::Orientation::RIGHT_TURN); + assert(tangent_orientation_1 == CGAL::Orientation::LEFT_TURN || tangent_orientation_1 == CGAL::Orientation::RIGHT_TURN); + + Vector_2 tangent_vec_0 = calculate_parabolic_tangent_vector(p, f_0, u_0, v_0, tangent_orientation_0); + Vector_2 tangent_vec_1 = calculate_parabolic_tangent_vector(p, f_1, u_1, v_1, tangent_orientation_1); + + return CGAL::sign(tangent_vec_0.x() * tangent_vec_1.y() - tangent_vec_0.y() * tangent_vec_1.x()); + } + }; + + using ParabolicTangentToSegmentOrientationPredicateFiltered = CGAL::Filtered_predicate, ParabolicTangentToSegmentOrientationPredicate, C2E, C2F>; + using ParabolicTangentToParabolicTangentOrientationPredicateFiltered = CGAL::Filtered_predicate, ParabolicTangentToParabolicTangentOrientationPredicate, C2E, C2F>; +} // namespace impl + +using ParabolicTangentToSegmentOrientation = impl::ParabolicTangentToSegmentOrientationPredicateFiltered; +using ParabolicTangentToParabolicTangentOrientation = impl::ParabolicTangentToParabolicTangentOrientationPredicateFiltered; +using CGAL_Point = impl::K::Point_2; + +inline static CGAL_Point to_cgal_point(const VD::vertex_type *pt) { return {pt->x(), pt->y()}; } +inline static CGAL_Point to_cgal_point(const Point &pt) { return {pt.x(), pt.y()}; } +inline static CGAL_Point to_cgal_point(const Vec2d &pt) { return {pt.x(), pt.y()}; } + +inline static Linef make_linef(const VD::edge_type &edge) +{ + const VD::vertex_type *v0 = edge.vertex0(); + const VD::vertex_type *v1 = edge.vertex1(); + return {Vec2d(v0->x(), v0->y()), Vec2d(v1->x(), v1->y())}; +} + +inline static bool is_equal(const VD::vertex_type &first, const VD::vertex_type &second) { return first.x() == second.x() && first.y() == second.y(); } // FIXME Lukas H.: Also includes parabolic segments. bool VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(const VD &voronoi_diagram) { + using CGAL_Point = CGAL::Exact_predicates_exact_constructions_kernel::Point_2; + using CGAL_Segment = CGAL::Arr_segment_traits_2::Curve_2; + auto to_cgal_point = [](const VD::vertex_type &pt) -> CGAL_Point { return {pt.x(), pt.y()}; }; + assert(std::all_of(voronoi_diagram.edges().cbegin(), voronoi_diagram.edges().cend(), [](const VD::edge_type &edge) { return edge.color() == 0; })); @@ -30,7 +148,7 @@ bool VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(const VD &voronoi_ 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())) { + VoronoiUtils::is_finite(*edge.vertex0()) && 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); @@ -46,37 +164,101 @@ bool VoronoiUtilsCgal::is_voronoi_diagram_planar_intersection(const VD &voronoi_ 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); +struct ParabolicSegment +{ + const Point focus; + const Line directrix; + // Two points on the parabola; + const Linef segment; + // Indicate if focus point is on the left side or right side relative to parabolic segment endpoints. + const CGAL::Orientation is_focus_on_left; +}; + +inline static ParabolicSegment get_parabolic_segment(const VD::edge_type &edge, const std::vector &segments) +{ + assert(edge.is_curved()); + + const VD::cell_type *left_cell = edge.cell(); + const VD::cell_type *right_cell = edge.twin()->cell(); + + const Point focus_pt = VoronoiUtils::getSourcePoint(*(left_cell->contains_point() ? left_cell : right_cell), segments); + const VoronoiUtils::Segment &directrix = VoronoiUtils::getSourceSegment(*(left_cell->contains_point() ? right_cell : left_cell), segments); + CGAL::Orientation focus_side = CGAL::opposite(CGAL::orientation(to_cgal_point(edge.vertex0()), to_cgal_point(edge.vertex1()), to_cgal_point(focus_pt))); + + assert(focus_side == CGAL::Orientation::LEFT_TURN || focus_side == CGAL::Orientation::RIGHT_TURN); + return {focus_pt, Line(directrix.from(), directrix.to()), make_linef(edge), focus_side}; +} + +inline static CGAL::Orientation orientation_of_two_edges(const VD::edge_type &edge_a, const VD::edge_type &edge_b, const std::vector &segments) { + assert(is_equal(*edge_a.vertex0(), *edge_b.vertex0())); + CGAL::Orientation orientation; + if (edge_a.is_linear() && edge_b.is_linear()) { + orientation = CGAL::orientation(to_cgal_point(edge_a.vertex0()), to_cgal_point(edge_a.vertex1()), to_cgal_point(edge_b.vertex1())); + } else if (edge_a.is_curved() && edge_b.is_curved()) { + const ParabolicSegment parabolic_a = get_parabolic_segment(edge_a, segments); + const ParabolicSegment parabolic_b = get_parabolic_segment(edge_b, segments); + orientation = ParabolicTangentToParabolicTangentOrientation{}(to_cgal_point(parabolic_a.segment.a), + to_cgal_point(parabolic_a.focus), + to_cgal_point(parabolic_a.directrix.a), + to_cgal_point(parabolic_a.directrix.b), + parabolic_a.is_focus_on_left, + to_cgal_point(parabolic_b.focus), + to_cgal_point(parabolic_b.directrix.a), + to_cgal_point(parabolic_b.directrix.b), + parabolic_b.is_focus_on_left); + return orientation; + } else { + assert(edge_a.is_curved() != edge_b.is_curved()); + + const VD::edge_type &linear_edge = edge_a.is_curved() ? edge_b : edge_a; + const VD::edge_type ¶bolic_edge = edge_a.is_curved() ? edge_a : edge_b; + const ParabolicSegment parabolic = get_parabolic_segment(parabolic_edge, segments); + orientation = ParabolicTangentToSegmentOrientation{}(to_cgal_point(parabolic.segment.a), to_cgal_point(linear_edge.vertex1()), + to_cgal_point(parabolic.focus), + to_cgal_point(parabolic.directrix.a), + to_cgal_point(parabolic.directrix.b), + parabolic.is_focus_on_left); + + if (edge_b.is_curved()) + orientation = CGAL::opposite(orientation); + } + + return orientation; +} + +static bool check_if_three_edges_are_ccw(const VD::edge_type &first, const VD::edge_type &second, const VD::edge_type &third, const std::vector &segments) +{ + assert(is_equal(*first.vertex0(), *second.vertex0()) && is_equal(*second.vertex0(), *third.vertex0())); + + CGAL::Orientation orientation = orientation_of_two_edges(first, second, segments); 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; + return orientation_of_two_edges(first, third, segments) == 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); + CGAL::Orientation orientation1 = orientation_of_two_edges(first, third, segments); + CGAL::Orientation orientation2 = orientation_of_two_edges(second, third, segments); 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); + CGAL::Orientation orientation1 = orientation_of_two_edges(first, third, segments); + CGAL::Orientation orientation2 = orientation_of_two_edges(second, third, segments); return (orientation1 == CGAL::Orientation::RIGHT_TURN || orientation2 == CGAL::Orientation::LEFT_TURN); } } -bool VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(const VoronoiDiagram &voronoi_diagram) +bool VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(const VoronoiDiagram &voronoi_diagram, const std::vector &segments) { 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())) + if (edge->is_finite() && edge->vertex0() != nullptr && edge->vertex1() != nullptr && + VoronoiUtils::is_finite(*edge->vertex0()) && VoronoiUtils::is_finite(*edge->vertex1())) edges.emplace_back(edge); edge = edge->rot_next(); @@ -89,8 +271,7 @@ bool VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(const VoronoiDiagram &vor 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()))) + if (!check_if_three_edges_are_ccw(*prev_edge, *curr_edge, *next_edge, segments)) return false; } } @@ -99,5 +280,4 @@ bool VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(const VoronoiDiagram &vor 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 index 897891bd9..cad54615b 100644 --- a/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp +++ b/src/libslic3r/Geometry/VoronoiUtilsCgal.hpp @@ -2,6 +2,7 @@ #define slic3r_VoronoiUtilsCgal_hpp_ #include "Voronoi.hpp" +#include "../Arachne/utils/VoronoiUtils.hpp" namespace Slic3r::Geometry { class VoronoiDiagram; @@ -13,7 +14,7 @@ public: 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); + static bool is_voronoi_diagram_planar_angle(const VoronoiDiagram &voronoi_diagram, const std::vector &segments); }; } // namespace Slic3r::Geometry From 22091a9e3eff2796d342acb7acd8ee17c6bf4356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Wed, 21 Sep 2022 14:08:30 +0200 Subject: [PATCH 4/5] Refactored code for the detection of degenerated Voronoi diagrams. Added detection for cases when some Voronoi edge is intersection input segments, which should fix another case reported in #8846. --- .../Arachne/SkeletalTrapezoidation.cpp | 122 +++++++++++++----- .../Arachne/SkeletalTrapezoidation.hpp | 7 +- 2 files changed, 92 insertions(+), 37 deletions(-) diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp index 34caee311..cd58f34c6 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.cpp @@ -573,6 +573,59 @@ inline static void rotate_back_skeletal_trapezoidation_graph_after_fix(SkeletalT } } +bool detect_voronoi_edge_intersecting_input_segment(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments) +{ + for (VoronoiUtils::vd_t::cell_type cell : voronoi_diagram.cells()) { + if (!cell.incident_edge()) + continue; // Degenerated cell, there is no spoon + + if (!cell.contains_segment()) + continue; // Skip cells that don't contain segments. + + const VoronoiUtils::Segment &source_segment = VoronoiUtils::getSourceSegment(cell, segments); + const Vec2d source_segment_from = source_segment.from().cast(); + const Vec2d source_segment_vec = source_segment.to().cast() - source_segment_from; + + Point start_source_point, end_source_point; + VoronoiUtils::vd_t::edge_type *begin_voronoi_edge = nullptr, *end_voronoi_edge = nullptr; + SkeletalTrapezoidation::computeSegmentCellRange(cell, start_source_point, end_source_point, begin_voronoi_edge, end_voronoi_edge, segments); + // All Voronoi vertices must be on left side of the source segment, otherwise Voronoi diagram is invalid. + // FIXME Lukas H.: Be aware that begin_voronoi_edge and end_voronoi_edge could be nullptr in some specific cases. + // It mostly happens when there is some missing Voronoi, for example, in GH issue #8846 (IssuesWithMysteriousPerimeters.3mf). + if (begin_voronoi_edge != nullptr && end_voronoi_edge != nullptr) + for (VoronoiUtils::vd_t::edge_type *edge = begin_voronoi_edge; edge != end_voronoi_edge; edge = edge->next()) + if (const Vec2d edge_v1(edge->vertex1()->x(), edge->vertex1()->y()); Slic3r::cross2(source_segment_vec, edge_v1 - source_segment_from) < 0) + return true; + } + + return false; +} + +enum class VoronoiDiagramStatus { + NO_ISSUE_DETECTED, + MISSING_VORONOI_VERTEX, + NON_PLANAR_VORONOI_DIAGRAM, + VORONOI_EDGE_INTERSECTING_INPUT_SEGMENT, + OTHER_TYPE_OF_VORONOI_DIAGRAM_DEGENERATION +}; + +// Try to detect cases when some Voronoi vertex is missing, when the Voronoi diagram +// is not planar or some Voronoi edge is intersecting input segment. +VoronoiDiagramStatus detect_voronoi_diagram_known_issues(const Geometry::VoronoiDiagram &voronoi_diagram, + const std::vector &segments) +{ + if (const bool has_missing_voronoi_vertex = detect_missing_voronoi_vertex(voronoi_diagram, segments); has_missing_voronoi_vertex) { + return VoronoiDiagramStatus::MISSING_VORONOI_VERTEX; + } else if (const bool has_voronoi_edge_intersecting_input_segment = detect_voronoi_edge_intersecting_input_segment(voronoi_diagram, segments); has_voronoi_edge_intersecting_input_segment) { + // Detection if Voronoi edge is intersecting input segment detects at least one model in GH issue #8446. + return VoronoiDiagramStatus::VORONOI_EDGE_INTERSECTING_INPUT_SEGMENT; + } else if (const bool is_voronoi_diagram_planar = Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments); !is_voronoi_diagram_planar) { + // Detection of non-planar Voronoi diagram detects at least GH issues #8474, #8514 and #8446. + return VoronoiDiagramStatus::NON_PLANAR_VORONOI_DIAGRAM; + } + return VoronoiDiagramStatus::NO_ISSUE_DETECTED; +} + void SkeletalTrapezoidation::constructFromPolygons(const Polygons& polys) { #ifdef ARACHNE_DEBUG @@ -614,36 +667,35 @@ void SkeletalTrapezoidation::constructFromPolygons(const Polygons& 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, #8446 and #8846. - const bool is_voronoi_diagram_planar = Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments); - const double fix_angle = PI / 6; + // When any Voronoi vertex is missing, the Voronoi diagram is not planar, or some voronoi edge is + // intersecting input segment, rotate the input polygon and try again. + VoronoiDiagramStatus status = detect_voronoi_diagram_known_issues(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 || !is_voronoi_diagram_planar) { - if (has_missing_voronoi_vertex) + if (status != VoronoiDiagramStatus::NO_ISSUE_DETECTED) { + if (status == VoronoiDiagramStatus::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) + else if (status == VoronoiDiagramStatus::NON_PLANAR_VORONOI_DIAGRAM) BOOST_LOG_TRIVIAL(warning) << "Detected non-planar Voronoi diagram, input polygons will be rotated back and forth."; + else if (status == VoronoiDiagramStatus::VORONOI_EDGE_INTERSECTING_INPUT_SEGMENT) + BOOST_LOG_TRIVIAL(warning) << "Detected Voronoi edge intersecting input segment, 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(!detect_missing_voronoi_vertex(voronoi_diagram, segments)); assert(Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments)); + assert(!detect_voronoi_edge_intersecting_input_segment(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."; else if (!Geometry::VoronoiUtilsCgal::is_voronoi_diagram_planar_angle(voronoi_diagram, segments)) BOOST_LOG_TRIVIAL(error) << "Detected non-planar Voronoi diagram even after the rotation of input."; + else if (detect_voronoi_edge_intersecting_input_segment(voronoi_diagram, segments)) + BOOST_LOG_TRIVIAL(error) << "Detected Voronoi edge intersecting input segment 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()) { @@ -652,35 +704,35 @@ process_voronoi_diagram: Point start_source_point; Point end_source_point; - vd_t::edge_type* starting_vonoroi_edge = nullptr; - vd_t::edge_type* ending_vonoroi_edge = nullptr; + vd_t::edge_type* starting_voronoi_edge = nullptr; + vd_t::edge_type* ending_voronoi_edge = nullptr; // Compute and store result in above variables - + if (cell.contains_point()) { - const bool keep_going = computePointCellRange(cell, start_source_point, end_source_point, starting_vonoroi_edge, ending_vonoroi_edge, segments); + const bool keep_going = computePointCellRange(cell, start_source_point, end_source_point, starting_voronoi_edge, ending_voronoi_edge, segments); if (!keep_going) continue; } else { assert(cell.contains_segment()); - computeSegmentCellRange(cell, start_source_point, end_source_point, starting_vonoroi_edge, ending_vonoroi_edge, segments); + computeSegmentCellRange(cell, start_source_point, end_source_point, starting_voronoi_edge, ending_voronoi_edge, segments); } - - if (!starting_vonoroi_edge || !ending_vonoroi_edge) { + + if (!starting_voronoi_edge || !ending_voronoi_edge) { assert(false && "Each cell should start / end in a polygon vertex"); continue; } - + // Copy start to end edge to graph edge_t* prev_edge = nullptr; - assert(VoronoiUtils::p(starting_vonoroi_edge->vertex1()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex1()).x() >= std::numeric_limits::lowest()); - assert(VoronoiUtils::p(starting_vonoroi_edge->vertex1()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex1()).y() >= std::numeric_limits::lowest()); - transferEdge(start_source_point, VoronoiUtils::p(starting_vonoroi_edge->vertex1()).cast(), *starting_vonoroi_edge, prev_edge, start_source_point, end_source_point, segments); - node_t* starting_node = vd_node_to_he_node[starting_vonoroi_edge->vertex0()]; + assert(VoronoiUtils::p(starting_voronoi_edge->vertex1()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_voronoi_edge->vertex1()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(starting_voronoi_edge->vertex1()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_voronoi_edge->vertex1()).y() >= std::numeric_limits::lowest()); + transferEdge(start_source_point, VoronoiUtils::p(starting_voronoi_edge->vertex1()).cast(), *starting_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); + 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); - for (vd_t::edge_type* vd_edge = starting_vonoroi_edge->next(); vd_edge != ending_vonoroi_edge; vd_edge = vd_edge->next()) { + for (vd_t::edge_type* vd_edge = starting_voronoi_edge->next(); vd_edge != ending_voronoi_edge; vd_edge = vd_edge->next()) { assert(vd_edge->is_finite()); assert(VoronoiUtils::p(vd_edge->vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(vd_edge->vertex0()).x() >= std::numeric_limits::lowest()); @@ -692,12 +744,12 @@ process_voronoi_diagram: Point v2 = VoronoiUtils::p(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_vonoroi_edge); + graph.makeRib(prev_edge, start_source_point, end_source_point, vd_edge->next() == ending_voronoi_edge); } - assert(VoronoiUtils::p(starting_vonoroi_edge->vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex0()).x() >= std::numeric_limits::lowest()); - assert(VoronoiUtils::p(starting_vonoroi_edge->vertex0()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_vonoroi_edge->vertex0()).y() >= std::numeric_limits::lowest()); - transferEdge(VoronoiUtils::p(ending_vonoroi_edge->vertex0()).cast(), end_source_point, *ending_vonoroi_edge, prev_edge, start_source_point, end_source_point, segments); + assert(VoronoiUtils::p(starting_voronoi_edge->vertex0()).x() <= std::numeric_limits::max() && VoronoiUtils::p(starting_voronoi_edge->vertex0()).x() >= std::numeric_limits::lowest()); + assert(VoronoiUtils::p(starting_voronoi_edge->vertex0()).y() <= std::numeric_limits::max() && VoronoiUtils::p(starting_voronoi_edge->vertex0()).y() >= std::numeric_limits::lowest()); + transferEdge(VoronoiUtils::p(ending_voronoi_edge->vertex0()).cast(), end_source_point, *ending_voronoi_edge, prev_edge, start_source_point, end_source_point, segments); prev_edge->to->data.distance_to_boundary = 0; } @@ -705,9 +757,9 @@ process_voronoi_diagram: // 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)) { + if (status == VoronoiDiagramStatus::NO_ISSUE_DETECTED && 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; + status = VoronoiDiagramStatus::OTHER_TYPE_OF_VORONOI_DIAGRAM_DEGENERATION; 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)); @@ -724,14 +776,14 @@ process_voronoi_diagram: goto process_voronoi_diagram; } - if (degenerated_voronoi_diagram) { + if (status != VoronoiDiagramStatus::NO_ISSUE_DETECTED) { 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) + if (status != VoronoiDiagramStatus::NO_ISSUE_DETECTED) rotate_back_skeletal_trapezoidation_graph_after_fix(this->graph, fix_angle, vertex_mapping); #ifdef ARACHNE_DEBUG @@ -742,7 +794,7 @@ process_voronoi_diagram: graph.collapseSmallEdges(); - // Set [incident_edge] the the first possible edge that way we can iterate over all reachable edges from node.incident_edge, + // Set [incident_edge] the first possible edge that way we can iterate over all reachable edges from node.incident_edge, // without needing to iterate backward for (edge_t& edge : graph.edges) if (!edge.prev) diff --git a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp index 819b71367..b4029d586 100644 --- a/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp +++ b/src/libslic3r/Arachne/SkeletalTrapezoidation.hpp @@ -9,6 +9,7 @@ #include // smart pointers #include #include // pair +#include #include "utils/HalfEdgeGraph.hpp" #include "utils/PolygonsSegmentIndex.hpp" @@ -229,7 +230,7 @@ protected: * /return Whether the cell is inside of the polygon. If it's outside of the * polygon we should skip processing it altogether. */ - bool 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); + static bool 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); /*! * Compute the range of line segments that surround a cell of the skeletal @@ -255,7 +256,7 @@ protected: * /return Whether the cell is inside of the polygon. If it's outside of the * polygon we should skip processing it altogether. */ - void computeSegmentCellRange(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); + static void computeSegmentCellRange(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); /*! * 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 @@ -597,6 +598,8 @@ protected: * Genrate small segments for local maxima where the beading would only result in a single bead */ void generateLocalMaximaSingleBeads(); + + friend bool detect_voronoi_edge_intersecting_input_segment(const Geometry::VoronoiDiagram &voronoi_diagram, const std::vector &segments); }; } // namespace Slic3r::Arachne From 3bf6714d52f436cb6eb74f4ead558855735e61a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Hejl?= Date: Wed, 11 Jan 2023 15:54:07 +0100 Subject: [PATCH 5/5] Added several test cases for degenerated Voronoi diagrams extracted from #8446, #8846 and #9357. --- tests/libslic3r/test_arachne.cpp | 206 ++++++++++++++++++++++++++++++- 1 file changed, 205 insertions(+), 1 deletion(-) diff --git a/tests/libslic3r/test_arachne.cpp b/tests/libslic3r/test_arachne.cpp index af48c71a1..c4564f77c 100644 --- a/tests/libslic3r/test_arachne.cpp +++ b/tests/libslic3r/test_arachne.cpp @@ -478,7 +478,7 @@ TEST_CASE("Arachne - #8849 - Missing part of model", "[ArachneMissingPart8849]") export_perimeters_to_svg(debug_out_path("arachne-missing-part-8849.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); #endif - int64_t total_extrusion_length= 0; + int64_t total_extrusion_length = 0; for (Arachne::VariableWidthLines &perimeter : perimeters) for (Arachne::ExtrusionLine &extrusion_line : perimeter) total_extrusion_length += extrusion_line.getLength(); @@ -486,3 +486,207 @@ TEST_CASE("Arachne - #8849 - Missing part of model", "[ArachneMissingPart8849]") // Total extrusion length should be around 30mm when the part is missing and around 120 when everything is ok. // REQUIRE(total_extrusion_length >= scaled(120.)); } + +// This test case was distilled from GitHub issue #8446. +// Boost Voronoi generator produces non-planar Voronoi diagram with two intersecting linear Voronoi edges. +// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be. +TEST_CASE("Arachne - #8446 - Degenerated Voronoi diagram - Linear edges", "[ArachneDegeneratedDiagram8446LinearEdges]") { + Polygon poly_0 = { + Point( 42240656, 9020315), + Point( 4474248, 42960681), + Point( -4474248, 42960681), + Point( -4474248, 23193537), + Point( -6677407, 22661038), + Point( -8830542, 21906307), + Point( -9702935, 21539826), + Point(-13110431, 19607811), + Point(-18105334, 15167780), + Point(-20675743, 11422461), + Point(-39475413, 17530840), + Point(-42240653, 9020315) + }; + + Polygons polygons = {poly_0}; + coord_t ext_perimeter_spacing = 407079; + coord_t perimeter_spacing = 407079; + coord_t inset_count = 1; + + Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults()); + wall_tool_paths.generate(); + std::vector perimeters = wall_tool_paths.getToolPaths(); + +#ifdef ARACHNE_DEBUG_OUT + export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8446-linear-edges.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); +#endif + + int64_t total_extrusion_length = 0; + for (Arachne::VariableWidthLines &perimeter : perimeters) + for (Arachne::ExtrusionLine &extrusion_line : perimeter) + total_extrusion_length += extrusion_line.getLength(); + + // Total extrusion length should be around 211.2mm when the part is ok and 212.1mm when it has perimeters in places where they shouldn't be. + REQUIRE(total_extrusion_length <= scaled(211.5)); +} + +// This test case was distilled from GitHub issue #8846. +// Boost Voronoi generator produces degenerated Voronoi diagram with one parabolic edge intersecting linear Voronoi edge. +// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be. +TEST_CASE("Arachne - #8846 - Degenerated Voronoi diagram - One Parabola", "[ArachneDegeneratedDiagram8846OneParabola]") { + const Polygon poly_0 = { + Point(101978540, -41304489), Point(101978540, 41304489), + Point(94709788, 42514051), Point(94709788, 48052315), + Point(93352716, 48052315), Point(93352716, 42514052), + Point(75903540, 42514051), Point(75903540, 48052315), + Point(74546460, 48052315), Point(74546460, 42514052), + Point(69634788, 42514051), Point(69634788, 48052315), + Point(68277708, 48052315), Point(68277708, 42514051), + Point(63366040, 42514051), Point(63366040, 48052315), + Point(62008960, 48052315), Point(62008960, 42514051), + Point(57097292, 42514051), Point(57097292, 48052315), + Point(55740212, 48052315), Point(55740212, 42514052), + Point(50828540, 42514052), Point(50828540, 48052315), + Point(49471460, 48052315), Point(49471460, 42514051), + Point(25753540, 42514051), Point(25753540, 48052315), + Point(24396460, 48052315), Point(24396460, 42514051), + Point(19484790, 42514052), Point(19484790, 48052315), + Point(18127710, 48052315), Point(18127710, 42514051), + Point(-5590210, 42514051), Point(-5590210, 48052315), + Point(-6947290, 48052315), Point(-6947290, 42514051), + Point(-11858960, 42514051), Point(-11858960, 48052315), + Point(-13216040, 48052315), Point(-13216040, 42514051), + Point(-18127710, 42514051), Point(-18127710, 48052315), + Point(-19484790, 48052315), Point(-19484790, 42514052), + Point(-49471460, 42514051), Point(-49471460, 48052315), + Point(-50828540, 48052315), Point(-50828540, 42514052), + Point(-55740212, 42514052), Point(-55740212, 48052315), + Point(-57097292, 48052315), Point(-57097292, 42514051), + Point(-68277708, 42514051), Point(-68277708, 48052315), + Point(-69634788, 48052315), Point(-69634788, 42514051), + Point(-74546460, 42514052), Point(-74546460, 48052315), + Point(-75903540, 48052315), Point(-75903540, 42514051), + Point(-80815204, 42514051), Point(-80815204, 48052315), + Point(-82172292, 48052315), Point(-82172292, 42514051), + Point(-87083956, 42514051), Point(-87083956, 48052315), + Point(-88441044, 48052315), Point(-88441044, 42514051), + Point(-99621460, 42514051), Point(-99621460, 48052315), + Point(-100978540, 48052315), Point(-100978540, 42528248), + Point(-101978540, 41304489), Point(-101978540, -41304489), + Point(-100978540, -48052315), Point(-99621460, -48052315), + }; + + Polygon poly_1 = { + Point(-100671460, -40092775), + Point(-100671460, 40092775), + Point(100671460, 40092775), + Point(100671460, -40092775), + }; + + Polygons polygons = {poly_0, poly_1}; + coord_t ext_perimeter_spacing = 607079; + coord_t perimeter_spacing = 607079; + coord_t inset_count = 1; + + Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults()); + wall_tool_paths.generate(); + std::vector perimeters = wall_tool_paths.getToolPaths(); + +#ifdef ARACHNE_DEBUG_OUT + export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8846-one-parabola.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); +#endif + + int64_t total_extrusion_length = 0; + for (Arachne::VariableWidthLines &perimeter : perimeters) + for (Arachne::ExtrusionLine &extrusion_line : perimeter) + total_extrusion_length += extrusion_line.getLength(); + + // Total extrusion length should be around 1335mm when the part is ok and 1347mm when it has perimeters in places where they shouldn't be. + REQUIRE(total_extrusion_length <= scaled(1335.)); +} + +// This test case was distilled from GitHub issue #9357. +// Boost Voronoi generator produces degenerated Voronoi diagram with two intersecting parabolic Voronoi edges. +// Those intersecting edges are causing that perimeters are also generated in places where they shouldn't be. +TEST_CASE("Arachne - #9357 - Degenerated Voronoi diagram - Two parabolas", "[ArachneDegeneratedDiagram9357TwoParabolas]") { + const Polygon poly_0 = { + Point(78998946, -11733905), + Point(40069507, -7401251), + Point(39983905, -6751055), + Point(39983905, 8251054), + Point(79750000, 10522762), + Point(79983905, 10756667), + Point(79983905, 12248946), + Point(79950248, 12504617), + Point(79709032, 12928156), + Point(79491729, 13102031), + Point(78998946, 13233905), + Point(38501054, 13233905), + Point(37258117, 12901005), + Point(36349000, 11991885), + Point(36100868, 11392844), + Point(36016095, 10748947), + Point(36016095, -6751054), + Point(35930493, -7401249), + Point(4685798, -11733905), + }; + + Polygons polygons = {poly_0}; + coord_t ext_perimeter_spacing = 407079; + coord_t perimeter_spacing = 407079; + coord_t inset_count = 1; + + Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.2, PrintObjectConfig::defaults(), PrintConfig::defaults()); + wall_tool_paths.generate(); + std::vector perimeters = wall_tool_paths.getToolPaths(); + +#ifdef ARACHNE_DEBUG_OUT + export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-9357-two-parabolas.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); +#endif + + int64_t total_extrusion_length = 0; + for (Arachne::VariableWidthLines &perimeter : perimeters) + for (Arachne::ExtrusionLine &extrusion_line : perimeter) + total_extrusion_length += extrusion_line.getLength(); + + // Total extrusion length should be around 256mm when the part is ok and 293mm when it has perimeters in places where they shouldn't be. + REQUIRE(total_extrusion_length <= scaled(256.)); +} + +// This test case was distilled from GitHub issue #8846. +// Boost Voronoi generator produces degenerated Voronoi diagram with some Voronoi edges intersecting input segments. +// Those Voronoi edges intersecting input segments are causing that perimeters are also generated in places where they shouldn't be. +TEST_CASE("Arachne - #8846 - Degenerated Voronoi diagram - Voronoi edges intersecting input segment", "[ArachneDegeneratedDiagram8846IntersectingInputSegment]") { + const Polygon poly_0 = { + Point( 60000000, 58000000), + Point(-20000000, 53229451), + Point( 49312250, 53229452), + Point( 49443687, 53666225), + Point( 55358348, 50908580), + Point( 53666223, 49443687), + Point( 53229452, 49312250), + Point( 53229452, -49312250), + Point( 53666014, -49443623), + Point(-10000000, -58000000), + Point( 60000000, -58000000), + }; + + Polygons polygons = {poly_0}; + coord_t ext_perimeter_spacing = 407079; + coord_t perimeter_spacing = 407079; + coord_t inset_count = 1; + + Arachne::WallToolPaths wall_tool_paths(polygons, ext_perimeter_spacing, perimeter_spacing, inset_count, 0, 0.32, PrintObjectConfig::defaults(), PrintConfig::defaults()); + wall_tool_paths.generate(); + std::vector perimeters = wall_tool_paths.getToolPaths(); + +#ifdef ARACHNE_DEBUG_OUT + export_perimeters_to_svg(debug_out_path("arachne-degenerated-diagram-8846-intersecting-input-segment.svg"), polygons, perimeters, union_ex(wall_tool_paths.getInnerContour())); +#endif + + int64_t total_extrusion_length = 0; + for (Arachne::VariableWidthLines &perimeter : perimeters) + for (Arachne::ExtrusionLine &extrusion_line : perimeter) + total_extrusion_length += extrusion_line.getLength(); + + // Total extrusion length should be around 500mm when the part is ok and 680mm when it has perimeters in places where they shouldn't be. + REQUIRE(total_extrusion_length <= scaled(500.)); +} \ No newline at end of file