From beae8aa5168be93ae56b013464a1b8316c40eccf Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 20 Oct 2026 02:03:52 +0800 Subject: [PATCH] Fix G-code export stalling for hours on layers with thousands of islands --- src/libslic3r/GCode/OrderingStrategies.cpp | 15 ++++-- src/libslic3r/GCode/OrderingStrategies.hpp | 4 +- tests/libslic3r/test_ordering_strategies.cpp | 49 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp index 98b8dfa37c..5d92f065b7 100644 --- a/src/libslic3r/GCode/OrderingStrategies.cpp +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -6,6 +6,7 @@ #include "../ShortestPath.hpp" #include "libslic3r/Point.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/libslic3r.h" #include #include @@ -120,7 +121,7 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers) // TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle. size_t n_edges = pn; - // Scan for first crossing; returns {i, j} or {npos, npos} if none. + // Scan for the first pair of intersecting edges worth reversing; returns {i, j} or {npos, npos} if none. auto find_crossing = [&]() -> std::pair { for (size_t i = 0; i < n_edges; ++i) { const Point& ai = centers[path[i]]; @@ -133,8 +134,13 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers) const Point& aj = centers[path[j]]; const Point& bj = centers[path[(j + 1) % pn]]; - if (!bboxes_overlap(ai, bi, aj, bj)) continue; - if (Geometry::segments_intersect(ai, bi, aj, bj)) + if (!bboxes_overlap(ai, bi, aj, bj) || !Geometry::segments_intersect(ai, bi, aj, bj)) + continue; + // The reversal replaces edges (ai, bi) and (aj, bj) with (ai, aj) and (bi, bj). Edges that only touch + // or overlap collinearly, as at points sharing a position, may gain nothing from it, and reversing + // them would find the same pair again on every scan. A gain within SCALED_EPSILON, which nearly + // collinear crossings can have too, is not worth a reversal either. + if (ai.distance_to(bi) + aj.distance_to(bj) - ai.distance_to(aj) - bi.distance_to(bj) > SCALED_EPSILON) return {i, j}; } } @@ -142,7 +148,8 @@ bool tsp_remove_crossings(std::vector& path, const Points& centers) }; // Process crossings one at a time: find first, reverse it, restart scan. - // Cap iterations to prevent infinite loops on collinear/overlapping segments. + // Every reversal shortens the cycle, so no tour repeats and the scan ends once no pair is worth + // reversing; the cap is a safety net. int max_iters = static_cast(pn * pn); bool improved = false; while (max_iters-- > 0) { diff --git a/src/libslic3r/GCode/OrderingStrategies.hpp b/src/libslic3r/GCode/OrderingStrategies.hpp index 95a6e77bcf..bf423779d7 100644 --- a/src/libslic3r/GCode/OrderingStrategies.hpp +++ b/src/libslic3r/GCode/OrderingStrategies.hpp @@ -25,8 +25,8 @@ namespace Slic3r { // Returns true if any improvement was made. bool tsp_2opt_improve(std::vector& path, const Points& centers, int max_passes = 10); -// Crossing removal: reverse any segment pair whose edges geometrically cross. -// Returns true if any crossing was removed. +// Crossing removal: reverse each pair of intersecting edges whose reversal shortens the cycle by more +// than SCALED_EPSILON. Returns true if any pair was reversed. bool tsp_remove_crossings(std::vector& path, const Points& centers); // The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only. diff --git a/tests/libslic3r/test_ordering_strategies.cpp b/tests/libslic3r/test_ordering_strategies.cpp index f4df7e8b5e..c9f169ff9d 100644 --- a/tests/libslic3r/test_ordering_strategies.cpp +++ b/tests/libslic3r/test_ordering_strategies.cpp @@ -1,6 +1,8 @@ #include #include #include +#include +#include #include #include #include @@ -136,6 +138,53 @@ TEST_CASE("tsp_remove_crossings eliminates crossings", "[TSPPostProcessing]") { REQUIRE(is_permutation(path, centers.size())); } +TEST_CASE("tsp_remove_crossings leaves a tour alone when no reversal shortens it", "[TSPPostProcessing]") { + // Three points on a diagonal, the middle one doubled as island centroids snapped to the same 1 mm cell are, + // and a corner closing the tour. Edges 0-1 and 2-3 touch at the doubled point, and reversing between them + // only swaps the two copies. + Points centers; + centers.emplace_back(0, 0); + centers.emplace_back(3000000, 3000000); + centers.emplace_back(3000000, 3000000); + centers.emplace_back(6000000, 6000000); + centers.emplace_back(6000000, 0); + std::vector path = {0, 1, 2, 3, 4}; + REQUIRE(Geometry::segments_intersect(centers[0], centers[1], centers[2], centers[3])); + + CHECK(!tsp_remove_crossings(path, centers)); + CHECK(path == std::vector{0, 1, 2, 3, 4}); +} + +TEST_CASE("tsp_remove_crossings keeps a crossing whose reversal gains less than SCALED_EPSILON", "[TSPPostProcessing]") { + // Two 100 mm edges crossing at a 10 nm offset: uncrossing them saves about 1e-6 scaled units of travel. + Points centers; + centers.emplace_back(0, 0); + centers.emplace_back(100000000, 10); + centers.emplace_back(100000000, 0); + centers.emplace_back(0, 10); + std::vector path = {0, 1, 2, 3}; + REQUIRE(Geometry::segments_intersect(centers[0], centers[1], centers[2], centers[3])); + + CHECK(!tsp_remove_crossings(path, centers)); + CHECK(path == std::vector{0, 1, 2, 3}); +} + +TEST_CASE("tsp_remove_crossings reverses overlapping collinear edges when that shortens the tour", "[TSPPostProcessing]") { + // Points at x = 0, 3, 1 and 2 mm on one line. Tour 0-3-1-2 is 8 mm long, and its edges [0,3] and [1,2] overlap. + // The shortest tour through points on a line is twice their 3 mm span. + Points centers; + centers.emplace_back(0, 0); + centers.emplace_back(3000000, 0); + centers.emplace_back(1000000, 0); + centers.emplace_back(2000000, 0); + std::vector path = {0, 1, 2, 3}; + REQUIRE_THAT(tsp_cycle_path_length(path, centers), Catch::Matchers::WithinAbs(8000000., 1.)); + + CHECK(tsp_remove_crossings(path, centers)); + REQUIRE(is_permutation(path, centers.size())); + CHECK_THAT(tsp_cycle_path_length(path, centers), Catch::Matchers::WithinAbs(6000000., 1.)); +} + TEST_CASE("tsp_rotate_minimize_closing shortens closing edge", "[TSPPostProcessing]") {