mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-04 08:42:10 +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/ToolOrdering.cpp
|
||||
GCode/ToolOrdering.hpp
|
||||
GCode/OrderingStrategies.cpp
|
||||
GCode/OrderingStrategies.hpp
|
||||
GCode/WipeTower2.cpp
|
||||
GCode/WipeTower2.hpp
|
||||
GCode/WipeTower.cpp
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "GCode/Thumbnails.hpp"
|
||||
#include "GCode/WipeTower.hpp"
|
||||
#include "ShortestPath.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "Utils.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_fan_mover.release();
|
||||
m_ordering_cache.clear();
|
||||
|
||||
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.
|
||||
// Therefore initialize the printing extruders from there.
|
||||
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.
|
||||
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
|
||||
: sort_object_instances_by_model_order(print);
|
||||
: sort_object_instances_by_model_order(print))));
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
if (initial_extruder_id == (unsigned int)-1) {
|
||||
// Nothing to print!
|
||||
@@ -5977,7 +5987,29 @@ LayerResult GCode::process_layer(
|
||||
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());
|
||||
|
||||
if (print.config().print_sequence == PrintSequence::ByObject) {
|
||||
|
||||
@@ -539,6 +539,11 @@ private:
|
||||
// Cache for custom seam enforcers/blockers for each layer.
|
||||
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;
|
||||
|
||||
|
||||
|
||||
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{
|
||||
{ "default", int(PrintOrder::Default) },
|
||||
{ "as_obj_list", int(PrintOrder::AsObjectList)},
|
||||
{ "best_of", int(PrintOrder::BestOfStrategies)},
|
||||
{ "snake", int(PrintOrder::Snake)},
|
||||
};
|
||||
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_values.push_back("default");
|
||||
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("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->set_default_value(new ConfigOptionEnum<PrintOrder>(PrintOrder::Default));
|
||||
|
||||
|
||||
@@ -214,6 +214,8 @@ enum class PrintOrder
|
||||
{
|
||||
Default,
|
||||
AsObjectList,
|
||||
BestOfStrategies, // run all custom strategies, pick the shortest total path
|
||||
Snake, // snake-like row traversal (back-and-forth) + 2-opt
|
||||
Count,
|
||||
};
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "KDTreeIndirect.hpp"
|
||||
#include "MutablePriorityQueue.hpp"
|
||||
#include "Print.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
@@ -1103,7 +1104,7 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy) {
|
||||
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]; };
|
||||
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());
|
||||
for (auto &segment_and_reversal : ordered)
|
||||
out.emplace_back(segment_and_reversal.first);
|
||||
|
||||
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
|
||||
// #define DEBUG_SVG_OUTPUT
|
||||
#endif /* NDEBUG */
|
||||
@@ -2025,12 +2043,13 @@ std::vector<const PrintInstance*> chain_print_object_instances(const std::vector
|
||||
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]; };
|
||||
std::vector<std::pair<size_t, bool>> ordered = chain_segments_greedy<Point, decltype(segment_end_point)>(segment_end_point, instances.size(), start_near);
|
||||
// Order objects using nearest neighbor + post-processing (crossing removal + 2-opt).
|
||||
std::vector<size_t> path = chain_points_with_postprocessing(object_reference_points, start_near);
|
||||
|
||||
std::vector<const PrintInstance*> out;
|
||||
out.reserve(instances.size());
|
||||
for (auto& segment_and_reversal : ordered) {
|
||||
const std::pair<size_t, size_t>& inst = instances[segment_and_reversal.first];
|
||||
out.reserve(path.size());
|
||||
for (size_t idx : path) {
|
||||
const std::pair<size_t, size_t>& inst = instances[idx];
|
||||
out.emplace_back(&print_objects[inst.first]->instances()[inst.second]);
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace Slic3r {
|
||||
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<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
|
||||
Reference in New Issue
Block a user