diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index 55bec2a862..812d28e088 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -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 diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index f45d807d43..fad4d4a106 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.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 new_ordering = chain_print_object_instances(print_objects, &wt_pos); + // Build cache key from sorted object IDs. + std::vector 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 new_ordering = cache_entry.second; std::reverse(new_ordering.begin(), new_ordering.end()); if (print.config().print_sequence == PrintSequence::ByObject) { diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 558d601e53..7f013f2425 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -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, std::vector>> + m_ordering_cache; + ExtrusionQualityEstimator m_extrusion_quality_estimator; diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp new file mode 100644 index 0000000000..6df47c084b --- /dev/null +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +namespace Slic3r { + +/* ==================================================================== + * TSP post-processing utilities + * ==================================================================== */ + +bool tsp_2opt_improve(std::vector& 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 el(pn); + for (size_t i = 0; i < pn; ++i) { + size_t ni = (i + 1) % pn; + el[i] = (centers[path[i]].cast() - centers[path[ni]].cast()).norm(); + } + return el; + }; + std::vector el = recompute_edges(); + + // Pre-compute squared edge lengths for early rejection in the inner loop. + auto recompute_edges_sq = [&]() { + std::vector elsq(pn); + for (size_t i = 0; i < pn; ++i) { + size_t ni = (i + 1) % pn; + elsq[i] = (centers[path[i]].cast() - centers[path[ni]].cast()).squaredNorm(); + } + return elsq; + }; + std::vector 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(); + const Vec2d& p_in = centers[path[(i + 1) % pn]].cast(); + 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(); + const Vec2d& p_jn = centers[path[j_next]].cast(); + 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& 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 { + 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::max(), std::numeric_limits::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(pn * pn); + bool improved = false; + while (max_iters-- > 0) { + auto [ci, cj] = find_crossing(); + if (ci == std::numeric_limits::max()) break; + improved = true; + std::reverse(path.begin() + ci + 1, path.begin() + cj + 1); + } + return improved; +} + +void tsp_rotate_minimize_closing(std::vector& path, const Points& centers) +{ + size_t pn = path.size(); + size_t best_start = 0; + double best_closing2 = std::numeric_limits::max(); + for (size_t start = 0; start < pn; ++start) { + size_t last = (start + pn - 1) % pn; + double d2 = (centers[path[start]].cast() - centers[path[last]].cast()).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 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& 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 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 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 group_into_rows(const Points& centers, double row_threshold) +{ + size_t n = centers.size(); + std::unordered_map> row_map; + for (size_t i = 0; i < n; ++i) { + int64_t y_key = static_cast(std::floor(static_cast(centers[i].y()) / row_threshold)); + row_map[y_key].push_back(i); + } + + std::vector 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(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 build_serpentine_path(const Points& centers, + std::vector& rows) +{ + std::vector 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() - centers[row.front()].cast()).squaredNorm(); + double dist_to_right = (prev_end.cast() - centers[row.back()].cast()).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 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 sorted_ys; + sorted_ys.reserve(n); + for (const auto& p : centers) sorted_ys.push_back(static_cast(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 snake_core(const Points& centers) +{ + if (centers.empty()) return {}; + + std::vector 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 chain_print_object_instances_snake(const std::vector& print_objects, const Point* start_near) +{ + return chain_instances_with_core(print_objects, start_near, snake_core); +} + +std::vector 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 chain_print_object_instances_best_of(const std::vector& print_objects, const Point* start_near) +{ + if (print_objects.empty()) + return {}; + + // Run all strategies. + std::vector> 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 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() - candidates[i][k]->shift.cast()).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(std::distance(metrics.begin(), best_it)); + + return candidates[best]; +} + +std::vector chain_print_object_instances_best_of(const Print& print) +{ + return chain_print_object_instances_best_of(print.objects().vector(), nullptr); +} + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/OrderingStrategies.hpp b/src/libslic3r/GCode/OrderingStrategies.hpp new file mode 100644 index 0000000000..f8e4661253 --- /dev/null +++ b/src/libslic3r/GCode/OrderingStrategies.hpp @@ -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 +#include +#include +#include + +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& 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& path, const Points& centers); + +// Rotate the cycle so the closing edge (last -> first) is minimized. +void tsp_rotate_minimize_closing(std::vector& path, const Points& centers); + +// Total Euclidean path length of a cycle (including closing edge). +inline double tsp_cycle_path_length(const std::vector& 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() - centers[path[next]].cast()).norm(); + } + return total; +} + +// Maximum edge length of a cycle (including closing edge). +inline double tsp_max_edge_length(const std::vector& 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() - centers[path[next]].cast()).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 +std::vector chain_instances_with_core( + const std::vector& print_objects, + const Point* start_near, + CoreFn&& core_fn) +{ + Points instance_centers; + std::vector> 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::max(); + for (size_t k = 0; k < instance_centers.size(); ++k) { + double d2 = (instance_centers[k].cast() - start_near->cast()).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 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 snake_core(const Points& centers); + +#ifndef SLIC3R_TEST_HARNESS + +// --- Production wrappers --- + +// Snake ordering. +std::vector chain_print_object_instances_snake(const std::vector& print_objects, const Point* start_near); +std::vector 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 chain_print_object_instances_best_of(const std::vector& print_objects, const Point* start_near); +std::vector chain_print_object_instances_best_of(const Print& print); + +#endif // SLIC3R_TEST_HARNESS + +} // namespace Slic3r + +#endif /* slic3r_OrderingStrategies_hpp_ */ diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ebcc2fa41a..823aec11d7 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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::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::Default)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index c4006b6c35..67841c6404 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -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, }; diff --git a/src/libslic3r/ShortestPath.cpp b/src/libslic3r/ShortestPath.cpp index e2fc258e31..eb3fc66927 100644 --- a/src/libslic3r/ShortestPath.cpp +++ b/src/libslic3r/ShortestPath.cpp @@ -10,6 +10,7 @@ #include "KDTreeIndirect.hpp" #include "MutablePriorityQueue.hpp" #include "Print.hpp" +#include "GCode/OrderingStrategies.hpp" #include #include @@ -1103,7 +1104,7 @@ std::vector chain_expolygons(const ExPolygons &input_exploy) { return chain_points(points); } -std::vector chain_points(const Points &points, Point *start_near) +std::vector 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> ordered = chain_segments_greedy(segment_end_point, points.size(), start_near); @@ -1111,9 +1112,26 @@ std::vector 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 chain_points_with_postprocessing(const Points &points, const Point *start_near) +{ + std::vector 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 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> ordered = chain_segments_greedy(segment_end_point, instances.size(), start_near); + // Order objects using nearest neighbor + post-processing (crossing removal + 2-opt). + std::vector path = chain_points_with_postprocessing(object_reference_points, start_near); + std::vector out; - out.reserve(instances.size()); - for (auto& segment_and_reversal : ordered) { - const std::pair& inst = instances[segment_and_reversal.first]; + out.reserve(path.size()); + for (size_t idx : path) { + const std::pair& inst = instances[idx]; out.emplace_back(&print_objects[inst.first]->instances()[inst.second]); } return out; diff --git a/src/libslic3r/ShortestPath.hpp b/src/libslic3r/ShortestPath.hpp index 88389b20de..5de7d8ef4a 100644 --- a/src/libslic3r/ShortestPath.hpp +++ b/src/libslic3r/ShortestPath.hpp @@ -15,7 +15,9 @@ namespace Slic3r { using PolyNodes = std::vector>; } -std::vector chain_points(const Points &points, Point *start_near = nullptr); +std::vector chain_points(const Points &points, const Point *start_near = nullptr); +// Variant with post-processing (crossing removal + 2-opt) for object ordering. +std::vector chain_points_with_postprocessing(const Points &points, const Point *start_near = nullptr); std::vector chain_expolygons(const ExPolygons &input_exploy); std::vector> chain_extrusion_entities(std::vector &entities, const Point *start_near = nullptr); diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 97d3576974..7524c27479 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -32,6 +32,7 @@ add_executable(${_TEST_NAME}_tests test_timeutils.cpp test_voronoi.cpp test_optimizers.cpp + test_ordering_strategies.cpp # test_png_io.cpp test_indexed_triangle_set.cpp ../libnest2d/printer_parts.cpp diff --git a/tests/libslic3r/test_ordering_strategies.cpp b/tests/libslic3r/test_ordering_strategies.cpp new file mode 100644 index 0000000000..ddbca07ae6 --- /dev/null +++ b/tests/libslic3r/test_ordering_strategies.cpp @@ -0,0 +1,297 @@ +#include + +#define SLIC3R_TEST_HARNESS + +#include "libslic3r/Point.hpp" +#include "libslic3r/GCode/OrderingStrategies.hpp" +#include "libslic3r/Geometry.hpp" + +#include +#include + +using namespace Slic3r; + +// --- Helpers --- + +static double euclidean_path_length(const std::vector& path, const Points& centers) +{ + return tsp_cycle_path_length(path, centers); +} + +static bool has_crossings(const std::vector& 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& path, size_t n) +{ + if (path.size() != n) return false; + std::unordered_set 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(R * std::cos(angle)), + static_cast(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(i * 2654435761u); + coord_t x = static_cast((h >> 16) & 0xFFFF) * 10; + coord_t y = static_cast(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 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 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 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::max(); + for (size_t start = 0; start < pn; ++start) { + size_t last = (start + pn - 1) % pn; + double d2 = (centers[path[start]].cast() - centers[path[last]].cast()).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() - centers[path.back()].cast()).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 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 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{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)); +}