Files
OrcaSlicer/tests/libslic3r/test_ordering_strategies.cpp
T
Ian BassiandTobias Gloth 69193d5681 Optimize travel: exact/refined path chaining, object tours, and joined infill lines (#16492)
* Shorten the travel between extrusion paths

chain_and_reorder_extrusion_entities() builds the print order of the infill,
ironing and support paths of a region from the nozzle position. It took the
greedy chain, which leaves its longest joins for last; it now shortens it:

- Up to eight paths, chain_segments_exact() finds the shortest chain over
  every order and allowed reversal, by dynamic programming over the subsets
  of paths, with a fixed start and a free end.
- Longer chains are refined by local search: 2-opt, or-opt of up to three
  paths, and moving a run of any length next to the nozzle to the front, each
  tried only toward the 32 end points nearest a link, which come from a
  uniform grid. A move is applied only if it shortens the chain, so the result
  is never longer than the greedy one.
- A nearest-next chain is built too, and refined and used instead when it is
  already shorter than the refined greedy chain, which happens when the greedy
  chain sweeps away from the nozzle and has to come back with a long jump.

The shared greedy chaining also chains two to four segments exactly, which
covers the islands of a layer and the loops of a perimeter collection. Paths
that cannot reverse, such as Grid's, are only reordered, and collections that
keep their order are left alone. The overload without a start point keeps the
greedy chain.

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>

* Order objects and islands along shorter tours

Objects under print_order Default, and the tour of islands across objects, are
visited in the same order on every layer, so chain_points_with_postprocessing()
orders them as a closed tour. Up to ten points, tsp_exact_cycle() finds the
shortest tour by dynamic programming over subsets, starting at the point
nearest the start point when one is given. Larger sets add or-opt to the
existing 2-opt and crossing removal: runs of up to three points move next to
one of the eight points nearest an end of the run, reversed or not, and
crossing removal runs last so no crossing is left. Snake ordering is
unchanged; Best of strategies gets the shorter Default tour as a candidate.

* Join infill lines into fewer paths

Every separate infill path costs a travel to it, so the fill generators now
leave fewer of them:

- connect_infill() pairs the line ends along each wall in the order that
  joins the most lines: besides shortest arc first, it replays arcs of about
  the same length taken along each contour, forwards and backwards, including
  the last pass that joins neighboring open ends, and keeps the order that
  joins the most. Regular patterns no longer leave loose ends where the
  pairs along a wall fall out of step, and multiline copies print out and back.
- Ends whose anchors would meet are joined along the wall instead, up to two
  anchors and a line width apart.
- An unpaired end anchors toward the nearest open end of another path, where
  the next path usually starts, instead of the side with more free wall. With
  multiline infill, an anchor toward a copy of the same line stops short of
  the copy instead of printing over it.
- Last, sparse infill lines whose ends are at most two line spacings apart are
  joined with a straight piece, shortest first, without closing a loop or
  crossing a line; with anchors off this is the only joining. Copies of
  neighboring multiline groups are joined at the same side, and lines that
  cannot reverse only join end to start.

* Describe the travel optimization design

How the extrusion chains, the object and island tours and the infill line joining shorten the travel, and why each part is built the way it is.

---------

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>
2026-10-19 15:53:32 -03:00

368 lines
11 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_all.hpp>
#include <vector>
#include <cstddef>
#include <math.h>
#include "libslic3r/libslic3r.h"
#include <cmath>
#include <cstdint>
#include <limits>
#define SLIC3R_TEST_HARNESS
#include "libslic3r/Point.hpp"
#include "libslic3r/GCode/OrderingStrategies.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/Geometry.hpp"
#include <algorithm>
#include <unordered_set>
#include <numeric>
#include <random>
#include <catch2/catch_approx.hpp>
using namespace Slic3r;
// --- Helpers ---
static double euclidean_path_length(const std::vector<size_t>& path, const Points& centers)
{
return tsp_cycle_path_length(path, centers);
}
static bool has_crossings(const std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
if (pn < 4) return false;
for (size_t i = 0; i < pn; ++i) {
size_t i_next = (i + 1) % pn;
for (size_t j = i + 2; j < pn; ++j) {
if (j == i_next) continue;
if (j == (pn - 1) && i == 0) continue;
size_t j_next = (j + 1) % pn;
if (Geometry::segments_intersect(
centers[path[i]], centers[path[i_next]],
centers[path[j]], centers[path[j_next]])) {
return true;
}
}
}
return false;
}
static bool is_permutation(const std::vector<size_t>& path, size_t n)
{
if (path.size() != n) return false;
std::unordered_set<size_t> seen(path.begin(), path.end());
for (size_t i = 0; i < n; ++i) {
if (seen.count(i) != 1) return false;
}
return true;
}
// --- Test fixtures ---
static Points make_grid_4x4()
{
Points pts;
for (int row = 0; row < 4; ++row)
for (int col = 0; col < 4; ++col)
pts.emplace_back(100000 * col, 100000 * row);
return pts;
}
static Points make_linear_5()
{
Points pts;
for (int i = 0; i < 5; ++i)
pts.emplace_back(100000 * i, 0);
return pts;
}
static Points make_ring_8()
{
Points pts;
constexpr double R = 100000.0;
for (int i = 0; i < 8; ++i) {
double angle = 2.0 * M_PI * i / 8.0;
pts.emplace_back(static_cast<coord_t>(R * std::cos(angle)),
static_cast<coord_t>(R * std::sin(angle)));
}
return pts;
}
static Points make_random_16()
{
// Deterministic "random" points via simple hash.
Points pts;
for (int i = 0; i < 16; ++i) {
uint32_t h = static_cast<uint32_t>(i * 2654435761u);
coord_t x = static_cast<coord_t>((h >> 16) & 0xFFFF) * 10;
coord_t y = static_cast<coord_t>(h & 0xFFFF) * 10;
pts.emplace_back(x, y);
}
return pts;
}
// --- TSP Post-Processing Tests ---
TEST_CASE("tsp_2opt_improve reduces path length", "[TSPPostProcessing]") {
Points centers = make_random_16();
std::vector<size_t> path(centers.size());
// Reverse half the path to create a deliberately bad ordering.
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
std::reverse(path.begin(), path.end() - path.size() / 2);
double before = euclidean_path_length(path, centers);
tsp_2opt_improve(path, centers);
double after = euclidean_path_length(path, centers);
REQUIRE(is_permutation(path, centers.size()));
CHECK(after <= before);
}
TEST_CASE("tsp_remove_crossings eliminates crossings", "[TSPPostProcessing]") {
Points centers = make_random_16();
std::vector<size_t> path(centers.size());
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
// Create a crossing by reversing a middle segment.
if (path.size() >= 4) {
std::reverse(path.begin() + 1, path.end() - 1);
}
tsp_remove_crossings(path, centers);
CHECK(!has_crossings(path, centers));
REQUIRE(is_permutation(path, centers.size()));
}
TEST_CASE("tsp_rotate_minimize_closing shortens closing edge", "[TSPPostProcessing]") {
Points centers = make_random_16();
std::vector<size_t> path(centers.size());
for (size_t i = 0; i < path.size(); ++i) path[i] = i;
// Compute all possible closing edge lengths.
size_t pn = path.size();
double min_closing2 = std::numeric_limits<double>::max();
for (size_t start = 0; start < pn; ++start) {
size_t last = (start + pn - 1) % pn;
double d2 = (centers[path[start]].cast<double>() - centers[path[last]].cast<double>()).squaredNorm();
if (d2 < min_closing2) min_closing2 = d2;
}
tsp_rotate_minimize_closing(path, centers);
// Closing edge should be the minimum possible.
double actual_closing2 = (centers[path.front()].cast<double>() - centers[path.back()].cast<double>()).squaredNorm();
CHECK(actual_closing2 == min_closing2);
REQUIRE(is_permutation(path, centers.size()));
}
TEST_CASE("tsp_cycle_path_length is correct for triangle", "[TSPPostProcessing]") {
Points pts;
pts.emplace_back(0, 0);
pts.emplace_back(100000, 0);
pts.emplace_back(50000, 86602); // equilateral ~100mm sides
std::vector<size_t> path = {0, 1, 2};
double len = tsp_cycle_path_length(path, pts);
// Perimeter of equilateral triangle with side ~100000.
REQUIRE(len > 290000);
REQUIRE(len < 310000);
}
TEST_CASE("tsp_max_edge_length finds longest edge", "[TSPPostProcessing]") {
Points pts;
pts.emplace_back(0, 0);
pts.emplace_back(100000, 0);
pts.emplace_back(50000, 0);
std::vector<size_t> path = {0, 1, 2};
double mx = tsp_max_edge_length(path, pts);
// Longest edge is 0->1 = 100000.
CHECK(mx == Catch::Approx(100000).margin(1));
}
// --- Core Strategy Tests: Empty / Small Inputs ---
TEST_CASE("snake_core handles empty input", "[Snake]") {
Points centers;
auto path = snake_core(centers);
REQUIRE(path.empty());
}
TEST_CASE("snake_core handles single point", "[Snake]") {
Points pts{{100, 200}};
CHECK(snake_core(pts) == std::vector<size_t>{0});
}
TEST_CASE("snake_core handles two points", "[Snake]") {
Points pts{{100, 200}, {300, 400}};
auto p2 = snake_core(pts);
REQUIRE(is_permutation(p2, 2));
}
// --- Core Strategy Tests: Grid Layout ---
TEST_CASE("snake produces good path on grid", "[Snake]") {
Points centers = make_grid_4x4();
auto path = snake_core(centers);
REQUIRE(is_permutation(path, centers.size()));
CHECK(!has_crossings(path, centers));
}
// --- Core Strategy Tests: Variable Row Spacing ---
TEST_CASE("snake handles variable Y spacing", "[Snake]") {
// Rows at Y = 0, 50, 100, 1000 (large gap between last two rows).
// The adaptive row detection should identify the tight cluster (0, 50, 100)
// and the isolated row (1000) without splitting them incorrectly.
Points pts;
pts.emplace_back(0, 0); pts.emplace_back(100000, 0);
pts.emplace_back(0, 50000); pts.emplace_back(100000, 50000);
pts.emplace_back(0, 100000); pts.emplace_back(100000, 100000);
pts.emplace_back(0, 1000000); pts.emplace_back(100000, 1000000);
auto path = snake_core(pts);
REQUIRE(is_permutation(path, pts.size()));
CHECK(!has_crossings(path, pts));
}
// --- Core Strategy Tests: All Points Same Y ---
TEST_CASE("snake handles all points on same Y", "[Snake]") {
// All points share the same Y coordinate. This exercises the
// division-by-zero guard (ys.size() == 1).
Points pts;
for (int i = 0; i < 6; ++i)
pts.emplace_back(100000 * i, 50000);
auto path = snake_core(pts);
REQUIRE(is_permutation(path, pts.size()));
}
// --- Core Strategy Tests: Collinear Points ---
TEST_CASE("snake_core handles collinear points", "[Snake]") {
Points centers = make_linear_5();
auto p2 = snake_core(centers);
REQUIRE(is_permutation(p2, centers.size()));
}
// --- Core Strategy Tests: Ring Layout ---
TEST_CASE("snake_core produces valid paths on ring", "[Snake]") {
Points centers = make_ring_8();
auto p2 = snake_core(centers);
REQUIRE(is_permutation(p2, centers.size()));
}
// --- Core Strategy Tests: Random Layout ---
TEST_CASE("snake_core produces valid paths on random input", "[Snake]") {
Points centers = make_random_16();
auto p2 = snake_core(centers);
REQUIRE(is_permutation(p2, centers.size()));
}
// --- Quality Comparison Tests ---
TEST_CASE("snake has no crossings on random input", "[Snake]") {
Points centers = make_random_16();
auto path = snake_core(centers);
REQUIRE(is_permutation(path, centers.size()));
CHECK(!has_crossings(path, centers));
}
// --- Edge Cases ---
TEST_CASE("snake_core handles duplicate points", "[Snake]") {
Points pts;
pts.emplace_back(100, 200);
pts.emplace_back(100, 200); // duplicate
pts.emplace_back(300, 400);
auto p2 = snake_core(pts);
REQUIRE(p2.size() == pts.size());
}
TEST_CASE("snake_core handles three points", "[Snake]") {
Points pts;
pts.emplace_back(0, 0);
pts.emplace_back(100000, 0);
pts.emplace_back(50000, 86602);
auto p2 = snake_core(pts);
REQUIRE(is_permutation(p2, 3));
}
// The shortest cycle through the points, trying every order.
static double shortest_cycle_length(const Points& centers)
{
std::vector<size_t> path(centers.size());
std::iota(path.begin(), path.end(), 0);
double best = std::numeric_limits<double>::max();
do {
best = std::min(best, tsp_cycle_path_length(path, centers));
} while (std::next_permutation(path.begin() + 1, path.end()));
return best;
}
static Points random_centers(unsigned seed, size_t count)
{
std::mt19937 rng(seed);
std::uniform_real_distribution<> coordinate(0., 200.);
Points centers;
for (size_t i = 0; i < count; ++i) {
// One draw per statement, as the evaluation order of function arguments is unspecified.
const double x = coordinate(rng);
const double y = coordinate(rng);
centers.emplace_back(Point::new_scale(x, y));
}
return centers;
}
TEST_CASE("tsp_exact_cycle finds the shortest cycle", "[TSPPostProcessing]") {
const size_t count = GENERATE(4, 5, 6, 8);
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, count);
const std::vector<size_t> path = tsp_exact_cycle(centers);
REQUIRE(is_permutation(path, count));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering tours up to ten points along the shortest cycle", "[TSPPostProcessing]") {
const bool from_start = GENERATE(false, true);
const Point start = Point::new_scale(-20., -20.);
for (unsigned seed = 1; seed <= 5; ++seed) {
const Points centers = random_centers(seed, 9);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, from_start ? &start : nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering of many points is never longer than the greedy cycle", "[TSPPostProcessing]") {
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, 150);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= euclidean_path_length(chain_points(centers, nullptr), centers) + SCALED_EPSILON);
CHECK_FALSE(has_crossings(path, centers));
}
}