Cyclic ordering (#13578)

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
Maksym Pyrozhok
2026-07-28 01:52:29 +03:00
committed by GitHub
parent 47ccca7f72
commit ef7bfeda9c
11 changed files with 897 additions and 11 deletions

View File

@@ -248,6 +248,8 @@ set(lisbslic3r_sources
GCode/Thumbnails.hpp GCode/Thumbnails.hpp
GCode/ToolOrdering.cpp GCode/ToolOrdering.cpp
GCode/ToolOrdering.hpp GCode/ToolOrdering.hpp
GCode/OrderingStrategies.cpp
GCode/OrderingStrategies.hpp
GCode/WipeTower2.cpp GCode/WipeTower2.cpp
GCode/WipeTower2.hpp GCode/WipeTower2.hpp
GCode/WipeTower.cpp GCode/WipeTower.cpp

View File

@@ -14,6 +14,7 @@
#include "GCode/Thumbnails.hpp" #include "GCode/Thumbnails.hpp"
#include "GCode/WipeTower.hpp" #include "GCode/WipeTower.hpp"
#include "ShortestPath.hpp" #include "ShortestPath.hpp"
#include "GCode/OrderingStrategies.hpp"
#include "Print.hpp" #include "Print.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
@@ -2804,6 +2805,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_role_based_fan_marker_layer.fill(-1); m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.release(); m_fan_mover.release();
m_ordering_cache.clear();
m_writer.set_is_bbl_machine(is_bbl_printers); m_writer.set_is_bbl_machine(is_bbl_printers);
@@ -3124,11 +3126,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// In non-sequential print, the printing extruders may have been modified by the extruder switches stored in Model::custom_gcode_per_print_z. // In non-sequential print, the printing extruders may have been modified by the extruder switches stored in Model::custom_gcode_per_print_z.
// Therefore initialize the printing extruders from there. // Therefore initialize the printing extruders from there.
this->set_extruders(tool_ordering.all_extruders()); this->set_extruders(tool_ordering.all_extruders());
print_object_instances_ordering = print_object_instances_ordering =
// By default, order object instances using a nearest neighbor search. // By default, order object instances using a nearest neighbor search.
print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print) (print.config().print_order == PrintOrder::Default ? chain_print_object_instances(print)
// Snake: serpentine row traversal + 2-opt
: (print.config().print_order == PrintOrder::Snake ? chain_print_object_instances_snake(print)
// Best of all: run every strategy, pick the shortest total path
: (print.config().print_order == PrintOrder::BestOfStrategies ? chain_print_object_instances_best_of(print)
// Otherwise same order as the object list // Otherwise same order as the object list
: sort_object_instances_by_model_order(print); : sort_object_instances_by_model_order(print))));
} }
if (initial_extruder_id == (unsigned int)-1) { if (initial_extruder_id == (unsigned int)-1) {
// Nothing to print! // Nothing to print!
@@ -5977,7 +5987,29 @@ LayerResult GCode::process_layer(
print_objects.push_back(print.get_object(obj_idx)); print_objects.push_back(print.get_object(obj_idx));
} }
std::vector<const PrintInstance *> new_ordering = chain_print_object_instances(print_objects, &wt_pos); // Build cache key from sorted object IDs.
std::vector<ObjectID> obj_ids;
for (const PrintObject* po : print_objects)
obj_ids.emplace_back(po->id());
std::sort(obj_ids.begin(), obj_ids.end());
// Check cache: reuse ordering if filament + object set unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
bool cache_hit = (cache_entry.first == obj_ids);
if (!cache_hit) {
// Compute fresh ordering and store in cache.
cache_entry.first = obj_ids;
cache_entry.second =
print.config().print_order == PrintOrder::Snake
? chain_print_object_instances_snake(print_objects, &wt_pos)
: (print.config().print_order == PrintOrder::BestOfStrategies
? chain_print_object_instances_best_of(print_objects, &wt_pos)
: chain_print_object_instances(print_objects, &wt_pos));
}
// Reverse a local copy; keep cached value intact for reuse.
std::vector<const PrintInstance *> new_ordering = cache_entry.second;
std::reverse(new_ordering.begin(), new_ordering.end()); std::reverse(new_ordering.begin(), new_ordering.end());
if (print.config().print_sequence == PrintSequence::ByObject) { if (print.config().print_sequence == PrintSequence::ByObject) {

View File

@@ -539,6 +539,11 @@ private:
// Cache for custom seam enforcers/blockers for each layer. // Cache for custom seam enforcers/blockers for each layer.
SeamPlacer m_seam_placer; SeamPlacer m_seam_placer;
// Cache per-filament ordering to avoid recomputing when object set is unchanged across layers.
// Key: filament_id. Value: {sorted ObjectIDs of objects present on this filament, cached ordering}.
std::map<unsigned int, std::pair<std::vector<ObjectID>, std::vector<const PrintInstance*>>>
m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator; ExtrusionQualityEstimator m_extrusion_quality_estimator;

View File

@@ -0,0 +1,377 @@
// Print-object ordering strategies: implementation.
// Consolidates TSP post-processing, Snake, and Best-of-Strategies.
#include "OrderingStrategies.hpp"
#include "../Geometry.hpp"
#include "../ShortestPath.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
#include <unordered_map>
#include <utility>
#include <vector>
namespace Slic3r {
/* ====================================================================
* TSP post-processing utilities
* ==================================================================== */
bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_passes)
{
size_t pn = path.size();
if (pn <= 2) return false;
// Pre-compute edge lengths once per pass to avoid redundant norm() calls.
auto recompute_edges = [&]() {
std::vector<double> el(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
el[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).norm();
}
return el;
};
std::vector<double> el = recompute_edges();
// Pre-compute squared edge lengths for early rejection in the inner loop.
auto recompute_edges_sq = [&]() {
std::vector<double> elsq(pn);
for (size_t i = 0; i < pn; ++i) {
size_t ni = (i + 1) % pn;
elsq[i] = (centers[path[i]].cast<double>() - centers[path[ni]].cast<double>()).squaredNorm();
}
return elsq;
};
std::vector<double> elsq = recompute_edges_sq();
bool improved = false;
for (int pass = 0; max_passes <= 0 || pass < max_passes; ++pass) {
size_t best_i = pn, best_j = pn;
double best_gain = 0;
for (size_t i = 0; i < pn; ++i) {
const Vec2d& pi = centers[path[i]].cast<double>();
const Vec2d& p_in = centers[path[(i + 1) % pn]].cast<double>();
double d_i = el[i];
double d_i_sq = elsq[i];
for (size_t j = i + 2; j < pn; ++j) {
size_t j_next = (j + 1) % pn;
// Skip the swap that would reverse the entire cycle (removes both
// edges (0,1) and (pn-1,0), equivalent to traversing the cycle backwards).
if (i == 0 && j_next == 0) continue;
const Vec2d& pj = centers[path[j]].cast<double>();
const Vec2d& p_jn = centers[path[j_next]].cast<double>();
double d_j = el[j];
// Early rejection using squared distances (avoids 2 sqrt calls).
double new_a_sq = (pj - pi).squaredNorm();
double new_b_sq = (p_jn - p_in).squaredNorm();
if (new_a_sq >= d_i_sq && new_b_sq >= elsq[j]) continue;
double new_a = std::sqrt(new_a_sq);
double new_b = std::sqrt(new_b_sq);
double gain = d_i + d_j - new_a - new_b;
if (gain > best_gain) {
best_gain = gain;
best_i = i; best_j = j;
}
}
}
if (best_i == pn) break;
improved = true;
// Reverse the best swap segment
std::reverse(path.begin() + best_i + 1, path.begin() + best_j + 1);
// Recompute edge lengths after reversal
el = recompute_edges();
elsq = recompute_edges_sq();
}
return improved;
}
// Fast bounding-box overlap test (rejects most non-intersecting pairs).
static inline bool bboxes_overlap(const Point& a, const Point& b, const Point& c, const Point& d)
{
return !(std::max(a.x(), b.x()) < std::min(c.x(), d.x()) ||
std::max(c.x(), d.x()) < std::min(a.x(), b.x()) ||
std::max(a.y(), b.y()) < std::min(c.y(), d.y()) ||
std::max(c.y(), d.y()) < std::min(a.y(), b.y()));
}
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
if (pn <= 3) return false;
size_t n_edges = pn - 1;
// Scan for first crossing; returns {i, j} or {npos, npos} if none.
auto find_crossing = [&]() -> std::pair<size_t, size_t> {
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[i + 1]];
for (size_t j = i + 2; j < n_edges; ++j) {
const Point& aj = centers[path[j]];
const Point& bj = centers[path[j + 1]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
return {i, j};
}
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
size_t best_start = 0;
double best_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 < best_closing2) { best_closing2 = d2; best_start = start; }
}
std::rotate(path.begin(), path.begin() + best_start, path.end());
}
/* ====================================================================
* Snake ordering
* ==================================================================== */
struct SnakeRow { double avg_y; std::vector<size_t> indices; };
// --- Row threshold computation ---
// Extract unique Y values and use the median gap between them to determine
// the row threshold.
static double compute_row_threshold(const std::vector<double>& sorted_ys,
double y_min, double y_max,
size_t n,
double fraction_of_y_range,
double min_threshold_um)
{
constexpr double MIN_GAP_FILTER = 1.0; // ignore sub-micron gaps (coord_t = 1/100mm)
// Extract unique Y values
std::vector<double> unique_ys;
unique_ys.reserve(sorted_ys.size());
unique_ys.push_back(sorted_ys[0]);
for (size_t i = 1; i < sorted_ys.size(); ++i) {
if (sorted_ys[i] - sorted_ys[i - 1] > MIN_GAP_FILTER)
unique_ys.push_back(sorted_ys[i]);
}
double fallback_threshold = (y_max - y_min) * fraction_of_y_range;
if (unique_ys.size() <= 1) {
return std::max(fallback_threshold, min_threshold_um);
}
// Compute gaps between consecutive unique Y values
std::vector<double> gaps;
gaps.reserve(unique_ys.size() - 1);
for (size_t i = 1; i < unique_ys.size(); ++i)
gaps.push_back(unique_ys[i] - unique_ys[i - 1]);
if (gaps.empty()) {
return std::max(fallback_threshold, min_threshold_um);
}
// Sort gaps to find the median
std::sort(gaps.begin(), gaps.end());
double median_gap = gaps[gaps.size() / 2];
double min_gap = gaps.front();
// Threshold: half the gap between consecutive unique Y values.
double threshold = (median_gap < min_gap * 1.5) ? min_gap * 0.5 : median_gap * 0.5;
bool has_row_structure;
if (unique_ys.size() * 2 <= n) {
has_row_structure = true;
} else {
// Single-column or sparse: uniform gaps indicate a deliberate grid
double max_gap = *std::max_element(gaps.begin(), gaps.end());
has_row_structure = (max_gap < min_gap * 2.0);
}
if (has_row_structure) {
// For grid-like data, use the gap-based threshold directly.
return threshold;
}
return std::max(fallback_threshold, min_threshold_um);
}
// --- Row grouping ---
// Bin points into rows by quantising Y / threshold
static std::vector<SnakeRow> group_into_rows(const Points& centers, double row_threshold)
{
size_t n = centers.size();
std::unordered_map<int64_t, std::vector<size_t>> row_map;
for (size_t i = 0; i < n; ++i) {
int64_t y_key = static_cast<int64_t>(std::floor(static_cast<double>(centers[i].y()) / row_threshold));
row_map[y_key].push_back(i);
}
std::vector<SnakeRow> rows;
rows.reserve(row_map.size());
for (auto& [key, indices] : row_map) {
double avg_y = std::accumulate(indices.begin(), indices.end(), 0.0,
[&](double acc, size_t idx) { return acc + static_cast<double>(centers[idx].y()); })
/ indices.size();
rows.push_back({avg_y, std::move(indices)});
}
std::sort(rows.begin(), rows.end(),
[](const SnakeRow& a, const SnakeRow& b) { return a.avg_y < b.avg_y; });
return rows;
}
// Sort each row by X and greedily pick the direction (left->right or right->left)
// that minimises the transition distance from the previous row's endpoint.
static std::vector<size_t> build_serpentine_path(const Points& centers,
std::vector<SnakeRow>& rows)
{
std::vector<size_t> path;
path.reserve(centers.size());
for (size_t ri = 0; ri < rows.size(); ++ri) {
auto& row = rows[ri].indices;
std::sort(row.begin(), row.end(),
[&](size_t a, size_t b) { return centers[a].x() < centers[b].x(); });
if (ri == 0) {
path.insert(path.end(), row.begin(), row.end());
} else {
const Point& prev_end = centers[path.back()];
double dist_to_left = (prev_end.cast<double>() - centers[row.front()].cast<double>()).squaredNorm();
double dist_to_right = (prev_end.cast<double>() - centers[row.back()].cast<double>()).squaredNorm();
if (dist_to_left <= dist_to_right)
path.insert(path.end(), row.begin(), row.end());
else
path.insert(path.end(), row.rbegin(), row.rend());
}
}
return path;
}
// Row-based serpentine traversal: detect rows, bin points, snake through them.
static std::vector<size_t> row_serpentine_path(const Points& centers,
double fraction_of_y_range = 0.02,
double min_threshold_um = 1e4)
{
if (centers.empty()) return {};
size_t n = centers.size();
// Collect and sort Y coordinates.
std::vector<double> sorted_ys;
sorted_ys.reserve(n);
for (const auto& p : centers) sorted_ys.push_back(static_cast<double>(p.y()));
std::sort(sorted_ys.begin(), sorted_ys.end());
auto [ymin, ymax] = std::minmax_element(sorted_ys.begin(), sorted_ys.end());
double y_min = *ymin, y_max = *ymax;
double row_threshold = compute_row_threshold(sorted_ys, y_min, y_max, n,
fraction_of_y_range, min_threshold_um);
auto rows = group_into_rows(centers, row_threshold);
return build_serpentine_path(centers, rows);
}
std::vector<size_t> snake_core(const Points& centers)
{
if (centers.empty()) return {};
std::vector<size_t> path = row_serpentine_path(centers);
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, centers);
improved |= tsp_remove_crossings(path, centers);
if (!improved) break;
}
return path;
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
return chain_instances_with_core(print_objects, start_near, snake_core);
}
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print)
{
return chain_print_object_instances_snake(print.objects().vector(), nullptr);
}
/* ====================================================================
* Best-of-strategies meta-strategy
* ==================================================================== */
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near)
{
if (print_objects.empty())
return {};
// Run all strategies.
std::vector<std::vector<const PrintInstance*>> candidates;
candidates.push_back(chain_print_object_instances(print_objects, start_near));
candidates.push_back(chain_print_object_instances_snake(print_objects, start_near));
// Compute metrics for each candidate.
struct Candidate { double total_len; double max_edge; };
std::vector<Candidate> metrics;
metrics.reserve(candidates.size());
for (size_t i = 0; i < candidates.size(); ++i) {
double total = 0.0;
double mx = 0.0;
for (size_t j = 0; j < candidates[i].size(); ++j) {
size_t k = (j + 1) % candidates[i].size();
double d = (candidates[i][j]->shift.cast<double>() - candidates[i][k]->shift.cast<double>()).norm();
total += d;
if (d > mx) mx = d;
}
metrics.push_back({total, mx});
}
// Pick shortest total path; tiebreak on smallest max edge.
auto best_it = std::min_element(metrics.begin(), metrics.end(),
[](const Candidate& a, const Candidate& b) {
return a.total_len < b.total_len ||
(a.total_len == b.total_len && a.max_edge < b.max_edge);
});
size_t best = static_cast<size_t>(std::distance(metrics.begin(), best_it));
return candidates[best];
}
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print)
{
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
}
} // namespace Slic3r

View File

@@ -0,0 +1,143 @@
// Print-object ordering strategies and shared TSP post-processing utilities.
#ifndef slic3r_OrderingStrategies_hpp_
#define slic3r_OrderingStrategies_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#ifndef SLIC3R_TEST_HARNESS
#include "../Print.hpp"
#endif
#include <algorithm>
#include <limits>
#include <utility>
#include <vector>
namespace Slic3r {
// --- Path improvement (operate on index vectors into `centers`) ---
// 2-opt improvement: reverses segments that reduce total cycle path length.
// Returns true if any improvement was made.
bool tsp_2opt_improve(std::vector<size_t>& 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.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
// Total Euclidean path length of a cycle (including closing edge).
inline double tsp_cycle_path_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double total = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
total += (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
}
return total;
}
// Maximum edge length of a cycle (including closing edge).
inline double tsp_max_edge_length(const std::vector<size_t>& path, const Points& centers)
{
if (path.size() < 2) return 0.0;
double mx = 0.0;
for (size_t i = 0; i < path.size(); ++i) {
size_t next = (i + 1) % path.size();
double d = (centers[path[i]].cast<double>() - centers[path[next]].cast<double>()).norm();
if (d > mx) mx = d;
}
return mx;
}
#ifndef SLIC3R_TEST_HARNESS
// --- Wrapper boilerplate ---
// Collect instance centers from PrintObjects, optionally pre-rotate to honour
// start_near, call a core algorithm, and map the result back to PrintInstance*.
template<typename CoreFn>
std::vector<const PrintInstance*> chain_instances_with_core(
const std::vector<const PrintObject*>& print_objects,
const Point* start_near,
CoreFn&& core_fn)
{
Points instance_centers;
std::vector<std::pair<size_t, size_t>> instances;
for (size_t i = 0; i < print_objects.size(); ++i) {
const PrintObject& object = *print_objects[i];
for (size_t j = 0; j < object.instances().size(); ++j) {
instance_centers.emplace_back(object.instances()[j].shift);
instances.emplace_back(i, j);
}
}
if (instance_centers.empty()) return {};
// If start_near is provided, pre-rotate so closest point is first.
if (start_near != nullptr) {
size_t best_start = 0;
double best_d2 = std::numeric_limits<double>::max();
for (size_t k = 0; k < instance_centers.size(); ++k) {
double d2 = (instance_centers[k].cast<double>() - start_near->cast<double>()).squaredNorm();
if (d2 < best_d2) { best_d2 = d2; best_start = k; }
}
std::rotate(instance_centers.begin(), instance_centers.begin() + best_start, instance_centers.end());
std::rotate(instances.begin(), instances.begin() + best_start, instances.end());
}
auto path = core_fn(instance_centers);
// Rotate the cycle so the first element is the best starting point.
// When start_near is provided, pick the point closest to it (preserving
// the pre-rotation). Otherwise minimise the closing edge.
if (start_near != nullptr && !path.empty()) {
// Pre-rotation already put the closest point at index 0.
// Find where index 0 appears in the path and rotate it to the front.
auto it = std::find(path.begin(), path.end(), size_t(0));
if (it != path.begin())
std::rotate(path.begin(), it, path.end());
} else {
tsp_rotate_minimize_closing(path, instance_centers);
}
std::vector<const PrintInstance*> out;
out.reserve(path.size());
for (size_t step : path) {
out.emplace_back(&print_objects[instances[step].first]->instances()[instances[step].second]);
}
return out;
}
#endif // SLIC3R_TEST_HARNESS
// --- Core algorithms (operate on raw Points, return index permutations) ---
// Snake ordering: row grouping + serpentine traversal + post-processing.
std::vector<size_t> snake_core(const Points& centers);
#ifndef SLIC3R_TEST_HARNESS
// --- Production wrappers ---
// Snake ordering.
std::vector<const PrintInstance*> chain_print_object_instances_snake(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print& print);
// Best-of-strategies: run all strategies and return the shortest result.
// Primary: shortest total path; secondary tiebreaker: smallest max edge.
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
#endif // SLIC3R_TEST_HARNESS
} // namespace Slic3r
#endif /* slic3r_OrderingStrategies_hpp_ */

View File

@@ -331,6 +331,8 @@ CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintSequence)
static t_config_enum_values s_keys_map_PrintOrder{ static t_config_enum_values s_keys_map_PrintOrder{
{ "default", int(PrintOrder::Default) }, { "default", int(PrintOrder::Default) },
{ "as_obj_list", int(PrintOrder::AsObjectList)}, { "as_obj_list", int(PrintOrder::AsObjectList)},
{ "best_of", int(PrintOrder::BestOfStrategies)},
{ "snake", int(PrintOrder::Snake)},
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintOrder) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(PrintOrder)
@@ -2003,8 +2005,12 @@ void PrintConfigDef::init_fff_params()
def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values();
def->enum_values.push_back("default"); def->enum_values.push_back("default");
def->enum_values.push_back("as_obj_list"); def->enum_values.push_back("as_obj_list");
def->enum_values.push_back("best_of");
def->enum_values.push_back("snake");
def->enum_labels.push_back(L("Default")); def->enum_labels.push_back(L("Default"));
def->enum_labels.push_back(L("As object list")); def->enum_labels.push_back(L("As object list"));
def->enum_labels.push_back(L("Best of all (shortest path)"));
def->enum_labels.push_back(L("Snake"));
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default)); def->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default));

View File

@@ -214,6 +214,8 @@ enum class PrintOrder
{ {
Default, Default,
AsObjectList, AsObjectList,
BestOfStrategies, // run all custom strategies, pick the shortest total path
Snake, // snake-like row traversal (back-and-forth) + 2-opt
Count, Count,
}; };

View File

@@ -10,6 +10,7 @@
#include "KDTreeIndirect.hpp" #include "KDTreeIndirect.hpp"
#include "MutablePriorityQueue.hpp" #include "MutablePriorityQueue.hpp"
#include "Print.hpp" #include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
#include <cmath> #include <cmath>
#include <cassert> #include <cassert>
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
return chain_points(points); return chain_points(points);
} }
std::vector<size_t> chain_points(const Points &points, Point *start_near) std::vector<size_t> chain_points(const Points &points, const Point *start_near)
{ {
auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; }; auto segment_end_point = [&points](size_t idx, bool /* first_point */) -> const Point& { return points[idx]; };
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near); std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, points.size(), start_near);
@@ -1111,9 +1112,26 @@ std::vector<size_t> chain_points(const Points &points, Point *start_near)
out.reserve(ordered.size()); out.reserve(ordered.size());
for (auto &segment_and_reversal : ordered) for (auto &segment_and_reversal : ordered)
out.emplace_back(segment_and_reversal.first); out.emplace_back(segment_and_reversal.first);
return out; return out;
} }
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
std::vector<size_t> path = chain_points(points, start_near);
// Alternate 2-opt and crossing removal until convergence.
// 2-opt can create new crossings, and crossing removal can create new
// opportunities for 2-opt improvement. Break early if neither improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
}
if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points);
return path;
}
#ifndef NDEBUG #ifndef NDEBUG
// #define DEBUG_SVG_OUTPUT // #define DEBUG_SVG_OUTPUT
#endif /* NDEBUG */ #endif /* NDEBUG */
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
instances.emplace_back(i, j); instances.emplace_back(i, j);
} }
} }
auto segment_end_point = [&object_reference_points](size_t idx, bool /* first_point */) -> const Point& { return object_reference_points[idx]; }; // Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near); std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
std::vector<const PrintInstance*> out; std::vector<const PrintInstance*> out;
out.reserve(instances.size()); out.reserve(path.size());
for (auto& segment_and_reversal : ordered) { for (size_t idx : path) {
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first]; const std::pair<size_t, size_t>& inst = instances[idx];
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]); out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
} }
return out; return out;

View File

@@ -15,7 +15,9 @@ namespace Slic3r {
using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>; using PolyNodes = std::vector<PolyNode*, PointsAllocator<PolyNode*>>;
} }
std::vector<size_t> chain_points(const Points &points, Point *start_near = nullptr); std::vector<size_t> chain_points(const Points &points, const Point *start_near = nullptr);
// Variant with post-processing (crossing removal + 2-opt) for object ordering.
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr);
std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy); std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr); std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);

View File

@@ -32,6 +32,7 @@ add_executable(${_TEST_NAME}_tests
test_timeutils.cpp test_timeutils.cpp
test_voronoi.cpp test_voronoi.cpp
test_optimizers.cpp test_optimizers.cpp
test_ordering_strategies.cpp
# test_png_io.cpp # test_png_io.cpp
test_indexed_triangle_set.cpp test_indexed_triangle_set.cpp
../libnest2d/printer_parts.cpp ../libnest2d/printer_parts.cpp

View File

@@ -0,0 +1,297 @@
#include <catch2/catch_all.hpp>
#define SLIC3R_TEST_HARNESS
#include "libslic3r/Point.hpp"
#include "libslic3r/GCode/OrderingStrategies.hpp"
#include "libslic3r/Geometry.hpp"
#include <algorithm>
#include <unordered_set>
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));
}