// 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; // Treat path as a cycle: include the closing edge (pn-1 -> 0), consistent with the other // TSP helpers (2-opt, closing-edge rotation) that operate on the full cycle. size_t n_edges = pn; // 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) % pn]]; for (size_t j = i + 2; j < n_edges; ++j) { // Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0. if (i == 0 && j == pn - 1) continue; const Point& aj = centers[path[j]]; const Point& bj = centers[path[(j + 1) % pn]]; 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); } /* ==================================================================== * Island-level ordering entry point * ==================================================================== */ std::vector order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near) { if (points.empty()) return {}; if (print_order != PrintOrder::Snake && print_order != PrintOrder::BestOfStrategies) // Nearest neighbor + post-processing; honours start_near natively. return chain_points_with_postprocessing(points, start_near); auto run_snake = [&points, start_near]() { std::vector path = snake_core(points); if (start_near != nullptr && !path.empty()) { // Start the cycle at the point closest to start_near. size_t best_start = 0; double best_d2 = std::numeric_limits::max(); for (size_t k = 0; k < points.size(); ++k) { double d2 = (points[k].cast() - start_near->cast()).squaredNorm(); if (d2 < best_d2) { best_d2 = d2; best_start = k; } } auto it = std::find(path.begin(), path.end(), best_start); if (it != path.begin() && it != path.end()) std::rotate(path.begin(), it, path.end()); } else { tsp_rotate_minimize_closing(path, points); } return path; }; if (print_order == PrintOrder::Snake) return run_snake(); // Best-of: pick the shortest total cycle; tiebreak on smallest max edge. std::vector> candidates; candidates.emplace_back(chain_points_with_postprocessing(points, start_near)); candidates.emplace_back(run_snake()); size_t best = 0; double best_len = std::numeric_limits::max(); double best_edge = std::numeric_limits::max(); for (size_t i = 0; i < candidates.size(); ++i) { double len = tsp_cycle_path_length(candidates[i], points); double edge = tsp_max_edge_length(candidates[i], points); if (len < best_len || (len == best_len && edge < best_edge)) { best_len = len; best_edge = edge; best = i; } } return candidates[best]; } } // namespace Slic3r