mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-29 13:52:07 +00:00
Cyclic ordering (#13578)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
377
src/libslic3r/GCode/OrderingStrategies.cpp
Normal file
377
src/libslic3r/GCode/OrderingStrategies.cpp
Normal 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
|
||||||
143
src/libslic3r/GCode/OrderingStrategies.hpp
Normal file
143
src/libslic3r/GCode/OrderingStrategies.hpp
Normal 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_ */
|
||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
297
tests/libslic3r/test_ordering_strategies.cpp
Normal file
297
tests/libslic3r/test_ordering_strategies.cpp
Normal 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));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user