mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-19 21:51:06 +00:00
* 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>
203 lines
8.6 KiB
C++
203 lines
8.6 KiB
C++
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/generators/catch_generators.hpp>
|
|
#include <catch2/matchers/catch_matchers.hpp>
|
|
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
|
|
|
#include "libslic3r/ExtrusionEntity.hpp"
|
|
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
|
#include "libslic3r/Point.hpp"
|
|
#include "libslic3r/Polyline.hpp"
|
|
#include "libslic3r/ShortestPath.hpp"
|
|
#include "libslic3r/libslic3r.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstddef>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <math.h>
|
|
#include <random>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
using namespace Slic3r;
|
|
|
|
namespace {
|
|
|
|
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
|
|
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
|
|
struct Paths
|
|
{
|
|
ExtrusionEntityCollection collection;
|
|
std::vector<ExtrusionEntity*> entities;
|
|
|
|
Paths(unsigned seed, size_t count, bool can_reverse)
|
|
{
|
|
std::mt19937 rng(seed);
|
|
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
|
|
Polylines lines;
|
|
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);
|
|
const double l = length(rng);
|
|
const double t = angle(rng);
|
|
const Vec2d a(x, y);
|
|
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
|
|
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
|
|
}
|
|
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
|
|
entities = collection.entities;
|
|
}
|
|
};
|
|
|
|
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
|
|
{
|
|
double length = 0.;
|
|
Point last = start;
|
|
for (const ExtrusionEntity *ee : entities) {
|
|
length += (ee->first_point() - last).cast<double>().norm();
|
|
last = ee->last_point();
|
|
}
|
|
return length;
|
|
}
|
|
|
|
// Travel of the chain that always goes on to the nearest free path end.
|
|
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
|
{
|
|
std::vector<char> taken(entities.size(), 0);
|
|
double length = 0.;
|
|
Point last = from;
|
|
for (size_t step = 0; step < entities.size(); ++ step) {
|
|
double best = std::numeric_limits<double>::max();
|
|
size_t best_idx = 0;
|
|
bool best_reversed = false;
|
|
for (size_t i = 0; i < entities.size(); ++ i)
|
|
for (const bool reversed : { false, true })
|
|
if (! taken[i] && (can_reverse || ! reversed)) {
|
|
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
|
|
if (d < best) {
|
|
best = d;
|
|
best_idx = i;
|
|
best_reversed = reversed;
|
|
}
|
|
}
|
|
taken[best_idx] = 1;
|
|
length += best;
|
|
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
|
|
}
|
|
return length;
|
|
}
|
|
|
|
// The shortest travel over every order and allowed reversal of the paths, from start.
|
|
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
|
|
{
|
|
std::vector<size_t> order(entities.size());
|
|
std::iota(order.begin(), order.end(), 0);
|
|
double best = std::numeric_limits<double>::max();
|
|
do {
|
|
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
|
|
double length = 0.;
|
|
Point last = from;
|
|
for (size_t k = 0; k < order.size(); ++ k) {
|
|
const ExtrusionEntity *ee = entities[order[k]];
|
|
const bool reversed = (mask >> k & 1) != 0;
|
|
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
|
|
last = reversed ? ee->first_point() : ee->last_point();
|
|
}
|
|
best = std::min(best, length);
|
|
}
|
|
} while (std::next_permutation(order.begin(), order.end()));
|
|
return best;
|
|
}
|
|
|
|
const Point start = Point::new_scale(-10., -10.);
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
|
|
{
|
|
const size_t count = GENERATE(2, 3, 10, 60, 300);
|
|
const bool can_reverse = GENERATE(true, false);
|
|
double greedy_total = 0., improved_total = 0.;
|
|
for (unsigned seed = 1; seed <= 20; ++ seed) {
|
|
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
|
|
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
|
|
chain_and_reorder_extrusion_entities(greedy.entities, &start);
|
|
chain_and_reorder_extrusion_entities(improved.entities, start);
|
|
|
|
std::vector<ExtrusionEntity*> sorted = improved.entities;
|
|
std::sort(sorted.begin(), sorted.end());
|
|
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
|
|
REQUIRE(sorted.size() == count);
|
|
|
|
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
|
|
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
|
|
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
|
|
greedy_total += greedy_travel;
|
|
improved_total += improved_travel;
|
|
}
|
|
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
|
|
if (count >= 10)
|
|
CHECK(improved_total < 0.95 * greedy_total);
|
|
}
|
|
|
|
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
|
|
{
|
|
Paths paths(7, 100, false);
|
|
std::map<const ExtrusionEntity*, Point> first_points;
|
|
for (const ExtrusionEntity *ee : paths.entities)
|
|
first_points.emplace(ee, ee->first_point());
|
|
|
|
chain_and_reorder_extrusion_entities(paths.entities, start);
|
|
|
|
for (const ExtrusionEntity *ee : paths.entities)
|
|
CHECK(ee->first_point() == first_points.at(ee));
|
|
}
|
|
|
|
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
|
|
{
|
|
const size_t count = GENERATE(2, 3, 4, 5, 6);
|
|
const bool can_reverse = GENERATE(true, false);
|
|
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
|
Paths paths(seed, count, can_reverse);
|
|
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
|
chain_and_reorder_extrusion_entities(paths.entities, start);
|
|
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
|
|
{
|
|
const size_t count = GENERATE(2, 3, 4);
|
|
const bool can_reverse = GENERATE(true, false);
|
|
for (unsigned seed = 1; seed <= 10; ++ seed) {
|
|
Paths paths(seed, count, can_reverse);
|
|
const double shortest = shortest_travel(paths.entities, can_reverse, start);
|
|
chain_and_reorder_extrusion_entities(paths.entities, &start);
|
|
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
|
|
{
|
|
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
|
|
const Point from = Point::new_scale(0., 0.);
|
|
for (const bool greedy : { false, true }) {
|
|
ExtrusionEntityCollection collection;
|
|
extrusion_entities_append_paths(collection.entities,
|
|
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
|
|
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
|
|
erInternalInfill, 0.1, 0.45f, 0.2f);
|
|
std::vector<ExtrusionEntity*> entities = collection.entities;
|
|
const double shortest = shortest_travel(entities, true, from);
|
|
if (greedy)
|
|
chain_and_reorder_extrusion_entities(entities, &from);
|
|
else
|
|
chain_and_reorder_extrusion_entities(entities, from);
|
|
CHECK(entities.back() == collection.entities.front());
|
|
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
|
|
}
|
|
}
|