mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 14:08:20 +00:00
Optimize travel: exact/refined path chaining, object tours, and joined infill lines (#16492)
* Shorten the travel between extrusion paths chain_and_reorder_extrusion_entities() builds the print order of the infill, ironing and support paths of a region from the nozzle position. It took the greedy chain, which leaves its longest joins for last; it now shortens it: - Up to eight paths, chain_segments_exact() finds the shortest chain over every order and allowed reversal, by dynamic programming over the subsets of paths, with a fixed start and a free end. - Longer chains are refined by local search: 2-opt, or-opt of up to three paths, and moving a run of any length next to the nozzle to the front, each tried only toward the 32 end points nearest a link, which come from a uniform grid. A move is applied only if it shortens the chain, so the result is never longer than the greedy one. - A nearest-next chain is built too, and refined and used instead when it is already shorter than the refined greedy chain, which happens when the greedy chain sweeps away from the nozzle and has to come back with a long jump. The shared greedy chaining also chains two to four segments exactly, which covers the islands of a layer and the loops of a perimeter collection. Paths that cannot reverse, such as Grid's, are only reordered, and collections that keep their order are left alone. The overload without a start point keeps the greedy chain. Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com> * Order objects and islands along shorter tours Objects under print_order Default, and the tour of islands across objects, are visited in the same order on every layer, so chain_points_with_postprocessing() orders them as a closed tour. Up to ten points, tsp_exact_cycle() finds the shortest tour by dynamic programming over subsets, starting at the point nearest the start point when one is given. Larger sets add or-opt to the existing 2-opt and crossing removal: runs of up to three points move next to one of the eight points nearest an end of the run, reversed or not, and crossing removal runs last so no crossing is left. Snake ordering is unchanged; Best of strategies gets the shorter Default tour as a candidate. * Join infill lines into fewer paths Every separate infill path costs a travel to it, so the fill generators now leave fewer of them: - connect_infill() pairs the line ends along each wall in the order that joins the most lines: besides shortest arc first, it replays arcs of about the same length taken along each contour, forwards and backwards, including the last pass that joins neighboring open ends, and keeps the order that joins the most. Regular patterns no longer leave loose ends where the pairs along a wall fall out of step, and multiline copies print out and back. - Ends whose anchors would meet are joined along the wall instead, up to two anchors and a line width apart. - An unpaired end anchors toward the nearest open end of another path, where the next path usually starts, instead of the side with more free wall. With multiline infill, an anchor toward a copy of the same line stops short of the copy instead of printing over it. - Last, sparse infill lines whose ends are at most two line spacings apart are joined with a straight piece, shortest first, without closing a loop or crossing a line; with anchors off this is the only joining. Copies of neighboring multiline groups are joined at the same side, and lines that cannot reverse only join end to start. * Describe the travel optimization design How the extrusion chains, the object and island tours and the infill line joining shorten the travel, and why each part is built the way it is. --------- Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>
This commit is contained in:
1 parent
cdb9bb57ae
commit
69193d5681
11 files changed
+1316
-29
No files matched your search
+256
-13
@@ -11,6 +11,10 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <tuple>
|
||||
#include "../AABBTreeLines.hpp"
|
||||
#include "../ClipperUtils.hpp"
|
||||
#include "../EdgeGrid.hpp"
|
||||
#include "../Geometry.hpp"
|
||||
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
|
||||
thick_polylines.clear();
|
||||
}
|
||||
else {
|
||||
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
|
||||
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
|
||||
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
|
||||
extrusion_entities_append_paths(
|
||||
eec->entities, std::move(polylines),
|
||||
params.extrusion_role,
|
||||
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
};
|
||||
|
||||
const double line_half_width = 0.5 * scale_(spacing);
|
||||
// Two ends whose anchors would meet are joined instead, with about as much wall.
|
||||
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
|
||||
|
||||
#if 0
|
||||
// Connection from end of one infill line to the start of another infill line.
|
||||
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
#endif
|
||||
|
||||
// Line ends not yet joined to another line along the contour.
|
||||
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
|
||||
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
|
||||
|
||||
struct Arc {
|
||||
ContourIntersectionPoint *intersection;
|
||||
double arc_length;
|
||||
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
|
||||
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
|
||||
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
|
||||
|
||||
// Lines the arcs would join in this order.
|
||||
auto count_joins = [&](const std::vector<Arc> &order) {
|
||||
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
|
||||
for (size_t i = 0; i < consumed.size(); ++ i)
|
||||
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
|
||||
std::vector<size_t> parent(infill_ordered.size());
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
size_t joins = 0;
|
||||
for (const Arc &arc : order) {
|
||||
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
|
||||
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
|
||||
continue;
|
||||
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
if (arc.arc_length < join_length) {
|
||||
parent[polyline2] = polyline1;
|
||||
++ joins;
|
||||
} else if (anchor_length <= SCALED_EPSILON)
|
||||
continue;
|
||||
consumed[end1] = consumed[end2] = 1;
|
||||
}
|
||||
// The last pass joins the open ends next to each other, also those of a multiline group.
|
||||
for (size_t end = 0; end < consumed.size(); ++ end) {
|
||||
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
|
||||
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
|
||||
continue;
|
||||
const double length = graph.boundary_params[cp.contour_idx].back();
|
||||
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
|
||||
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
|
||||
};
|
||||
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
|
||||
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
|
||||
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
|
||||
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
|
||||
consumed[end] = anchor_length > SCALED_EPSILON;
|
||||
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l > join_length)
|
||||
break;
|
||||
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
|
||||
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
|
||||
if (polyline1 == polyline2)
|
||||
continue;
|
||||
parent[polyline2] = polyline1;
|
||||
consumed[end] = consumed[other] = 1;
|
||||
++ joins;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return joins;
|
||||
};
|
||||
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
|
||||
size_t best_joins = count_joins(arches);
|
||||
const double bucket = 0.5 * scale_(spacing);
|
||||
for (const bool backwards : { false, true }) {
|
||||
std::vector<Arc> along_contour = arches;
|
||||
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
|
||||
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
|
||||
if (bucket_l != bucket_r)
|
||||
return bucket_l < bucket_r;
|
||||
if (l.intersection->contour_idx != r.intersection->contour_idx)
|
||||
return l.intersection->contour_idx < r.intersection->contour_idx;
|
||||
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
|
||||
});
|
||||
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
|
||||
best_joins = joins;
|
||||
arches = std::move(along_contour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
|
||||
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (polyline_idx1 != polyline_idx2) {
|
||||
Polyline &polyline1 = infill_ordered[polyline_idx1];
|
||||
Polyline &polyline2 = infill_ordered[polyline_idx2];
|
||||
if (arc.arc_length < anchor_length_max) {
|
||||
if (arc.arc_length < join_length) {
|
||||
// Not closing a loop, connecting the lines.
|
||||
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
|
||||
if (contour[cp1->point_idx] == polyline1.points.front())
|
||||
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline1, polyline2, contour, cp1, cp2, false);
|
||||
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
|
||||
// Mark the second polygon as merged with the first one.
|
||||
if (polyline_idx2 < polyline_idx1) {
|
||||
polyline2 = std::move(polyline1);
|
||||
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
}
|
||||
}
|
||||
|
||||
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
|
||||
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
|
||||
const double length = graph.boundary_params[from.contour_idx].back();
|
||||
double distance = 0.;
|
||||
const ContourIntersectionPoint *cp = &from;
|
||||
for (size_t step = 0; step < 64; ++ step) {
|
||||
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
|
||||
if (other == nullptr || other == &from)
|
||||
break;
|
||||
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
|
||||
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
|
||||
return distance;
|
||||
cp = other;
|
||||
}
|
||||
return std::numeric_limits<double>::max();
|
||||
};
|
||||
|
||||
// Connect the remaining open infill lines to the perimeter lines if possible.
|
||||
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
|
||||
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
|
||||
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
|
||||
bool connected = false;
|
||||
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
|
||||
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
|
||||
if (l == std::numeric_limits<double>::max() || l > join_length)
|
||||
break;
|
||||
// Take the complete contour.
|
||||
bool reversed = l == lprev;
|
||||
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
if (contour[cp2->point_idx] == polyline2.points.back())
|
||||
polyline2.reverse();
|
||||
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
|
||||
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
|
||||
if (polyline_idx < polyline_idx2) {
|
||||
// Mark the second polyline as merged with the first one.
|
||||
merged_with[polyline_idx2] = polyline_idx;
|
||||
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
|
||||
break;
|
||||
}
|
||||
if (! connected && anchor_length > SCALED_EPSILON) {
|
||||
// Which to take? One could optimize for:
|
||||
// 1) Shortest path
|
||||
// 2) Hook length
|
||||
// ...
|
||||
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
|
||||
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
|
||||
if (l > SCALED_EPSILON) {
|
||||
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
|
||||
else
|
||||
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
|
||||
// Hook toward the nearest open end of another polyline, where the next path likely starts.
|
||||
const double free_prev = contour_point.contour_not_taken_length_prev;
|
||||
const double free_next = contour_point.contour_not_taken_length_next;
|
||||
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
|
||||
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
|
||||
bool backwards = free_prev > free_next;
|
||||
if (to_prev < to_next && free_prev > SCALED_EPSILON)
|
||||
backwards = true;
|
||||
else if (to_next < to_prev && free_next > SCALED_EPSILON)
|
||||
backwards = false;
|
||||
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
|
||||
double length = anchor_length;
|
||||
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
|
||||
params.multiline > 1 && other != &contour_point) {
|
||||
const Point &end = contour[contour_point.point_idx];
|
||||
Vec2d dir = Vec2d::Zero();
|
||||
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
|
||||
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
|
||||
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
|
||||
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
|
||||
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
|
||||
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
|
||||
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
|
||||
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
|
||||
}
|
||||
}
|
||||
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
|
||||
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
|
||||
backwards, std::max(0., length), line_half_width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
|
||||
polylines = std::move(all_polylines);
|
||||
}
|
||||
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
|
||||
{
|
||||
const size_t n = polylines.size();
|
||||
if (n < 2 || max_gap <= 0.)
|
||||
return;
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
|
||||
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
|
||||
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
|
||||
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
|
||||
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
|
||||
std::unordered_map<uint64_t, std::vector<size_t>> grid;
|
||||
for (size_t e = 0; e < 2 * n; ++ e)
|
||||
if (polylines[e >> 1].size() >= 2)
|
||||
grid[key(cell_of(end_point(e)))].push_back(e);
|
||||
|
||||
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
|
||||
std::vector<char> backwards(n, 0);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (polylines[i].size() >= 2) {
|
||||
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
|
||||
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
|
||||
}
|
||||
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
|
||||
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
|
||||
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
|
||||
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
|
||||
for (const auto &[cell_key, ends] : grid) {
|
||||
const Vec2i64 c = cell_of(end_point(ends.front()));
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t a : ends)
|
||||
for (size_t b : it->second)
|
||||
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
|
||||
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
|
||||
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
|
||||
}
|
||||
if (gaps.empty())
|
||||
return;
|
||||
std::sort(gaps.begin(), gaps.end());
|
||||
|
||||
std::vector<size_t> partner(2 * n, npos), parent(n);
|
||||
std::iota(parent.begin(), parent.end(), 0);
|
||||
auto find = [&parent](size_t i) {
|
||||
while (parent[i] != i)
|
||||
i = parent[i] = parent[parent[i]];
|
||||
return i;
|
||||
};
|
||||
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
|
||||
// Only the segments through a cell next to a candidate join can cross one.
|
||||
std::unordered_set<uint64_t> near_gaps;
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
for (size_t e : { a, b })
|
||||
for (int64_t dx = -1; dx <= 1; ++ dx)
|
||||
for (int64_t dy = -1; dy <= 1; ++ dy)
|
||||
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
|
||||
Lines near_lines;
|
||||
for (const Polyline &pl : polylines)
|
||||
for (size_t i = 1; i < pl.size(); ++ i) {
|
||||
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
|
||||
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
|
||||
bool near = (hi - lo).maxCoeff() > 3;
|
||||
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
|
||||
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
|
||||
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
|
||||
if (near)
|
||||
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
|
||||
}
|
||||
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
|
||||
auto crosses = [&](size_t a, size_t b) {
|
||||
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
|
||||
const double length = (q - p).norm();
|
||||
if (length <= 2. * SCALED_EPSILON)
|
||||
return false;
|
||||
const Vec2d dir = (q - p) / length;
|
||||
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
|
||||
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
|
||||
return true;
|
||||
const Vec2i64 c = cell_of(end_point(a));
|
||||
for (int64_t dx = -3; dx <= 3; ++ dx)
|
||||
for (int64_t dy = -3; dy <= 3; ++ dy)
|
||||
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
|
||||
for (size_t e : it->second)
|
||||
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
|
||||
return true;
|
||||
return false;
|
||||
};
|
||||
for (const auto &[apart, trail, d, a, b] : gaps)
|
||||
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
|
||||
partner[a] = b;
|
||||
partner[b] = a;
|
||||
parent[find(a >> 1)] = find(b >> 1);
|
||||
}
|
||||
|
||||
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
|
||||
Polylines out;
|
||||
std::vector<char> done(n, 0);
|
||||
auto walk = [&](size_t first_end) {
|
||||
Polyline joined;
|
||||
for (size_t e = first_end; e != npos;) {
|
||||
Polyline &pl = polylines[e >> 1];
|
||||
done[e >> 1] = 1;
|
||||
if (e & 1)
|
||||
pl.reverse();
|
||||
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
|
||||
e = partner[e ^ 1];
|
||||
}
|
||||
out.emplace_back(std::move(joined));
|
||||
};
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i] == npos)
|
||||
walk(2 * i);
|
||||
for (size_t i = 0; i < n; ++ i)
|
||||
if (! done[i] && partner[2 * i + 1] == npos)
|
||||
walk(2 * i + 1);
|
||||
polylines = std::move(out);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -258,8 +258,11 @@ public:
|
||||
|
||||
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
|
||||
};
|
||||
//Fill Multiline
|
||||
//Fill Multiline
|
||||
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
|
||||
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
|
||||
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
|
||||
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_FillBase_hpp_
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -153,6 +154,41 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
|
||||
return improved;
|
||||
}
|
||||
|
||||
std::vector<size_t> tsp_exact_cycle(const Points& centers)
|
||||
{
|
||||
const size_t n = centers.size();
|
||||
std::vector<size_t> path(n);
|
||||
std::iota(path.begin(), path.end(), 0);
|
||||
if (n <= 3) return path;
|
||||
// State mask * m + j: the points 1..m of mask visited after point 0, the last one j + 1.
|
||||
const size_t m = n - 1;
|
||||
assert(m <= 16);
|
||||
std::vector<double> cost((size_t(1) << m) * m, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(cost.size(), size_t(-1));
|
||||
auto dist = [¢ers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
|
||||
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
|
||||
for (size_t j = 0; j < m; ++j)
|
||||
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < m; ++k)
|
||||
if (!(mask >> k & 1)) {
|
||||
const size_t next = (mask | size_t(1) << k) * m + k;
|
||||
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
|
||||
cost[next] = nc;
|
||||
from[next] = mask * m + j;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << m) - 1;
|
||||
size_t best = full * m;
|
||||
for (size_t j = 1; j < m; ++j)
|
||||
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
|
||||
best = full * m + j;
|
||||
for (size_t s = best, k = n; k-- > 1; s = from[s])
|
||||
path[k] = s % m + 1;
|
||||
return path;
|
||||
}
|
||||
|
||||
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
|
||||
{
|
||||
size_t pn = path.size();
|
||||
|
||||
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_
|
||||
// Returns true if any crossing was removed.
|
||||
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
|
||||
|
||||
// The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only.
|
||||
std::vector<size_t> tsp_exact_cycle(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);
|
||||
|
||||
|
||||
+484
-11
@@ -26,8 +26,10 @@
|
||||
#include "Print.hpp"
|
||||
#include "GCode/OrderingStrategies.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <deque>
|
||||
#include "MultiMaterialSegmentation.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -96,6 +98,46 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
return out;
|
||||
}
|
||||
|
||||
// The shortest chain over every order and allowed reversal, by dynamic programming over the subsets of segments, so only
|
||||
// for a few of them. end_points holds the first and the last point of each segment; the chain starts at start when given
|
||||
// and its end is free. Return index and "reversed" flag.
|
||||
static std::vector<std::pair<size_t, bool>> chain_segments_exact(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d *start)
|
||||
{
|
||||
const size_t n = could_reverse.size();
|
||||
assert(n >= 1 && n <= 12 && end_points.size() == 2 * n);
|
||||
// State (mask * n + j) * 2 + o: the segments of mask visited, the last one j, flipped when o.
|
||||
const size_t num_states = (size_t(1) << n) * n * 2;
|
||||
std::vector<double> cost(num_states, std::numeric_limits<double>::max());
|
||||
std::vector<size_t> from(num_states, size_t(-1));
|
||||
auto entry = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + o]; };
|
||||
auto exit = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + 1 - o]; };
|
||||
for (size_t j = 0; j < n; ++ j)
|
||||
for (size_t o = 0; o <= size_t(could_reverse[j] != 0); ++ o)
|
||||
cost[((size_t(1) << j) * n + j) * 2 + o] = start == nullptr ? 0. : (entry(j, o) - *start).norm();
|
||||
for (size_t mask = 1; mask < (size_t(1) << n); ++ mask)
|
||||
for (size_t s = mask * n * 2; s < (mask + 1) * n * 2; ++ s)
|
||||
if (cost[s] < std::numeric_limits<double>::max())
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
if (! (mask >> k & 1))
|
||||
for (size_t o = 0; o <= size_t(could_reverse[k] != 0); ++ o) {
|
||||
const size_t next = (((mask | size_t(1) << k) * n) + k) * 2 + o;
|
||||
const double c = cost[s] + (entry(k, o) - exit(s / 2 % n, s & 1)).norm();
|
||||
if (c < cost[next]) {
|
||||
cost[next] = c;
|
||||
from[next] = s;
|
||||
}
|
||||
}
|
||||
const size_t full = (size_t(1) << n) - 1;
|
||||
size_t best = full * n * 2;
|
||||
for (size_t s = best; s < num_states; ++ s)
|
||||
if (cost[s] < cost[best])
|
||||
best = s;
|
||||
std::vector<std::pair<size_t, bool>> out(n);
|
||||
for (size_t s = best, k = n; k -- > 0; s = from[s])
|
||||
out[k] = { s / 2 % n, (s & 1) != 0 };
|
||||
return out;
|
||||
}
|
||||
|
||||
// Chain perimeters (always closed) and thin fills (closed or open) using a greedy algorithm.
|
||||
// Solving a Traveling Salesman Problem (TSP) with the modification, that the sites are not always points, but points and segments.
|
||||
// Solving using a greedy algorithm, where a shortest edge is added to the solution if it does not produce a bifurcation or a cycle.
|
||||
@@ -104,6 +146,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
|
||||
// The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which
|
||||
// is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates
|
||||
// a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities.
|
||||
// Orca: two to four segments are chained exactly instead.
|
||||
template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc>
|
||||
std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near)
|
||||
{
|
||||
@@ -118,6 +161,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
|
||||
out.emplace_back(0, could_reverse_func(0) && start_near != nullptr &&
|
||||
(end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm());
|
||||
}
|
||||
else if (num_segments <= 4)
|
||||
{
|
||||
// Orca: so few segments are cheap to chain exactly. The first one is then not always the one nearest start_near.
|
||||
std::vector<Vec2d> end_points(2 * num_segments);
|
||||
std::vector<char> could_reverse(num_segments);
|
||||
for (size_t i = 0; i < num_segments; ++ i) {
|
||||
end_points[2 * i] = end_point_func(i, true).template cast<double>();
|
||||
end_points[2 * i + 1] = end_point_func(i, false).template cast<double>();
|
||||
could_reverse[i] = could_reverse_func(i);
|
||||
}
|
||||
const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast<double>()) : Vec2d::Zero();
|
||||
out = chain_segments_exact(end_points, could_reverse, start_near != nullptr ? &start : nullptr);
|
||||
}
|
||||
else
|
||||
{
|
||||
// End points of segments for the KD tree closest point search.
|
||||
@@ -1039,12 +1095,370 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc
|
||||
return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near);
|
||||
}
|
||||
|
||||
// The K points nearest to a query, nearest first, ties by index, from a uniform grid of about two points per cell.
|
||||
template<size_t K>
|
||||
class NearestPoints
|
||||
{
|
||||
public:
|
||||
static constexpr size_t npos = size_t(-1);
|
||||
|
||||
explicit NearestPoints(const std::vector<Vec2d> &points) : m_points(points)
|
||||
{
|
||||
BoundingBoxf bbox;
|
||||
for (const Vec2d &p : points)
|
||||
bbox.merge(p);
|
||||
const Vec2d size = bbox.size();
|
||||
const double count = double(std::max<size_t>(points.size(), 1));
|
||||
m_cell = std::max(std::sqrt(2. * size.x() * size.y() / count), std::max(size.x(), size.y()) / count);
|
||||
if (m_cell <= 0.)
|
||||
m_cell = 1.;
|
||||
m_origin = bbox.min;
|
||||
m_cols = int(size.x() / m_cell) + 1;
|
||||
m_rows = int(size.y() / m_cell) + 1;
|
||||
m_first.assign(size_t(m_cols) * m_rows + 1, 0);
|
||||
for (const Vec2d &p : points)
|
||||
++ m_first[cell(p) + 1];
|
||||
for (size_t c = 1; c < m_first.size(); ++ c)
|
||||
m_first[c] += m_first[c - 1];
|
||||
m_index.resize(points.size());
|
||||
std::vector<size_t> next(m_first.begin(), m_first.end() - 1);
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
m_index[next[cell(points[i])] ++] = i;
|
||||
}
|
||||
|
||||
// Indices of the nearest points that filter accepts, npos past the last one.
|
||||
template<typename Filter>
|
||||
std::array<size_t, K> query(const Vec2d &q, Filter filter) const
|
||||
{
|
||||
std::array<std::pair<double, size_t>, K> best;
|
||||
size_t found = 0;
|
||||
const int cx = int(std::clamp(std::floor((q.x() - m_origin.x()) / m_cell), 0., double(m_cols - 1)));
|
||||
const int cy = int(std::clamp(std::floor((q.y() - m_origin.y()) / m_cell), 0., double(m_rows - 1)));
|
||||
auto visit = [&](int x, int y) {
|
||||
if (x < 0 || y < 0 || x >= m_cols || y >= m_rows)
|
||||
return;
|
||||
const size_t c = size_t(y) * m_cols + x;
|
||||
for (size_t k = m_first[c]; k < m_first[c + 1]; ++ k) {
|
||||
const size_t i = m_index[k];
|
||||
if (! filter(i))
|
||||
continue;
|
||||
const std::pair<double, size_t> candidate((m_points[i] - q).squaredNorm(), i);
|
||||
if (found == K && ! (candidate < best[K - 1]))
|
||||
continue;
|
||||
auto it = std::upper_bound(best.begin(), best.begin() + found, candidate);
|
||||
std::move_backward(it, best.begin() + std::min(found, K - 1), best.begin() + std::min(found + 1, K));
|
||||
*it = candidate;
|
||||
found = std::min(found + 1, K);
|
||||
}
|
||||
};
|
||||
// Points beyond ring r are at least r cells away.
|
||||
for (int r = 0; r <= std::max(m_cols, m_rows); ++ r) {
|
||||
if (r == 0)
|
||||
visit(cx, cy);
|
||||
for (int d = -r; d <= r && r > 0; ++ d) {
|
||||
visit(cx + d, cy - r);
|
||||
visit(cx + d, cy + r);
|
||||
if (d != -r && d != r) {
|
||||
visit(cx - r, cy + d);
|
||||
visit(cx + r, cy + d);
|
||||
}
|
||||
}
|
||||
if (found == K && best[K - 1].first <= sqr(r * m_cell))
|
||||
break;
|
||||
}
|
||||
std::array<size_t, K> out;
|
||||
out.fill(npos);
|
||||
for (size_t k = 0; k < found; ++ k)
|
||||
out[k] = best[k].second;
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
size_t cell(const Vec2d &p) const
|
||||
{
|
||||
const int x = std::clamp(int((p.x() - m_origin.x()) / m_cell), 0, m_cols - 1);
|
||||
const int y = std::clamp(int((p.y() - m_origin.y()) / m_cell), 0, m_rows - 1);
|
||||
return size_t(y) * m_cols + x;
|
||||
}
|
||||
|
||||
const std::vector<Vec2d> &m_points;
|
||||
Vec2d m_origin;
|
||||
double m_cell;
|
||||
int m_cols;
|
||||
int m_rows;
|
||||
std::vector<size_t> m_first;
|
||||
std::vector<size_t> m_index;
|
||||
};
|
||||
|
||||
// Shortens the travel of a chain with 2-opt moves (reversing a run of segments) and or-opt moves (moving a run of up to
|
||||
// three segments elsewhere, or a longer one to the front, flipped or not). end_points holds the first and the last point
|
||||
// of each segment. The chain starts at start_near when given, and its end is free. A move that would flip a segment that
|
||||
// cannot reverse is skipped. Moves only create links between nearest neighbor end points, and after a move only the
|
||||
// segments next to a changed link are examined again, until a sweep of all segments finds no move. This keeps the search
|
||||
// close to linear.
|
||||
static void improve_chain_by_local_search(std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points,
|
||||
const std::vector<char> &could_reverse, const Vec2d *start_near)
|
||||
{
|
||||
const size_t n = chain.size();
|
||||
if (n < 2)
|
||||
return;
|
||||
assert(end_points.size() == 2 * n && could_reverse.size() == n);
|
||||
|
||||
std::vector<size_t> order(n), pos(n), fixed_before(n + 1, 0);
|
||||
std::vector<char> flipped(n);
|
||||
for (size_t k = 0; k < n; ++ k) {
|
||||
order[k] = chain[k].first;
|
||||
flipped[order[k]] = chain[k].second;
|
||||
}
|
||||
// Updates positions lo..hi after a move, which keeps the number of segments that cannot reverse past hi.
|
||||
auto refresh = [&](size_t lo, size_t hi) {
|
||||
for (size_t k = lo; k <= hi; ++ k) {
|
||||
pos[order[k]] = k;
|
||||
fixed_before[k + 1] = fixed_before[k] + ! could_reverse[order[k]];
|
||||
}
|
||||
};
|
||||
refresh(0, n - 1);
|
||||
auto entry_idx = [&](size_t k) { return 2 * order[k] + flipped[order[k]]; };
|
||||
auto exit_idx = [&](size_t k) { return 2 * order[k] + 1 - flipped[order[k]]; };
|
||||
auto entry = [&](size_t k) -> const Vec2d& { return end_points[entry_idx(k)]; };
|
||||
auto exit = [&](size_t k) -> const Vec2d& { return end_points[exit_idx(k)]; };
|
||||
auto is_exit = [&](size_t e) { return char(e & 1) != flipped[e >> 1]; };
|
||||
auto reversible = [&](size_t i, size_t j) { return fixed_before[j + 1] == fixed_before[i]; };
|
||||
auto dist = [](const Vec2d *a, const Vec2d *b) { return a == nullptr || b == nullptr ? 0. : (*a - *b).norm(); };
|
||||
// Travel end points around position k, null where the chain is open.
|
||||
auto prev_exit = [&](size_t k) -> const Vec2d* { return k > 0 ? &exit(k - 1) : start_near; };
|
||||
auto next_entry = [&](size_t k) -> const Vec2d* { return k + 1 < n ? &entry(k + 1) : nullptr; };
|
||||
|
||||
// The 32 nearest end points are the candidates, which covers every move on short chains; 8 missed much of the gain.
|
||||
static constexpr size_t K = 32;
|
||||
static constexpr size_t npos = NearestPoints<K>::npos;
|
||||
const NearestPoints<K> nearest(end_points);
|
||||
std::vector<std::array<size_t, K>> neighbors(end_points.size());
|
||||
for (size_t e = 0; e < end_points.size(); ++ e)
|
||||
neighbors[e] = nearest.query(end_points[e], [e](size_t idx) { return (idx >> 1) != (e >> 1); });
|
||||
std::array<size_t, K> start_neighbors;
|
||||
start_neighbors.fill(npos);
|
||||
if (start_near != nullptr)
|
||||
start_neighbors = nearest.query(*start_near, [](size_t) { return true; });
|
||||
|
||||
// Segments to examine, by id, each queued once.
|
||||
std::deque<size_t> queue;
|
||||
std::vector<char> queued(n, 0);
|
||||
auto enqueue = [&](size_t k) {
|
||||
if (k < n && ! queued[order[k]]) {
|
||||
queued[order[k]] = 1;
|
||||
queue.push_back(order[k]);
|
||||
}
|
||||
};
|
||||
|
||||
const double min_gain = SCALED_EPSILON;
|
||||
// Reverses positions i..j if that shortens the chain.
|
||||
auto try_2opt = [&](size_t i, size_t j) {
|
||||
if (! reversible(i, j))
|
||||
return false;
|
||||
const Vec2d *a = prev_exit(i), *b = next_entry(j);
|
||||
if (dist(a, &exit(j)) + dist(&entry(i), b) > dist(a, &entry(i)) + dist(&exit(j), b) - min_gain)
|
||||
return false;
|
||||
std::reverse(order.begin() + i, order.begin() + j + 1);
|
||||
for (size_t k = i; k <= j; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
refresh(i, j);
|
||||
for (size_t k : { i - 1, i, j, j + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// Moves positions i..last after position gap (size_t(-1) for the front) if that shortens the chain.
|
||||
auto try_oropt = [&](size_t i, size_t last, size_t gap) {
|
||||
if (gap + 1 == i || (gap >= i && gap <= last))
|
||||
return false;
|
||||
const Vec2d *a = gap == size_t(-1) ? start_near : &exit(gap);
|
||||
const Vec2d *b = gap + 1 < n ? &entry(gap + 1) : nullptr;
|
||||
const double removal_gain = dist(prev_exit(i), &entry(i)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(i), next_entry(last));
|
||||
const double base = dist(a, b);
|
||||
const double cost_kept = dist(a, &entry(i)) + dist(&exit(last), b) - base;
|
||||
const double cost_flipped = reversible(i, last) ? dist(a, &exit(last)) + dist(&entry(i), b) - base : std::numeric_limits<double>::max();
|
||||
if (std::min(cost_kept, cost_flipped) > removal_gain - min_gain)
|
||||
return false;
|
||||
const size_t len = last - i + 1;
|
||||
// The run lands on first..first+len-1; lo..hi spans every position that changed.
|
||||
const bool forward = gap > last && gap != size_t(-1);
|
||||
const size_t first = forward ? gap + 1 - len : gap + 1;
|
||||
const size_t lo = forward ? i : first;
|
||||
const size_t hi = forward ? gap : last;
|
||||
if (forward)
|
||||
std::rotate(order.begin() + i, order.begin() + last + 1, order.begin() + gap + 1);
|
||||
else
|
||||
std::rotate(order.begin() + first, order.begin() + i, order.begin() + last + 1);
|
||||
if (cost_flipped < cost_kept) {
|
||||
std::reverse(order.begin() + first, order.begin() + first + len);
|
||||
for (size_t k = first; k < first + len; ++ k)
|
||||
flipped[order[k]] ^= 1;
|
||||
}
|
||||
refresh(lo, hi);
|
||||
// The links around the run's new place, and the link closing its old place.
|
||||
for (size_t k : { first - 1, first, first + len - 1, first + len, forward ? i - 1 : last, forward ? i : last + 1 })
|
||||
enqueue(k);
|
||||
return true;
|
||||
};
|
||||
// The gap before position k of the chain with positions i..last taken out.
|
||||
auto gap_before = [](size_t k, size_t i, size_t last) { return k == last + 1 ? i - 1 : k - 1; };
|
||||
|
||||
// Applies the first move found that breaks a link next to position p.
|
||||
// Candidates are tried nearest first, while closer than the link the move would replace at that end point.
|
||||
auto closer = [&](const Vec2d &at, size_t q, double limit) { return q != npos && (end_points[q] - at).norm() < limit; };
|
||||
auto improve_at = [&](size_t p) {
|
||||
// 2-opt breaking the link into p, as the first or the last link of the reversed run.
|
||||
if (const Vec2d *a = prev_exit(p); a != nullptr) {
|
||||
const double old_link = dist(a, &entry(p));
|
||||
for (size_t q : p > 0 ? neighbors[exit_idx(p - 1)] : start_neighbors) {
|
||||
if (! closer(*a, q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] >= p && try_2opt(p, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
if (p > 0)
|
||||
for (size_t q : neighbors[entry_idx(p)]) {
|
||||
if (! closer(entry(p), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] < p && try_2opt(pos[q >> 1], p - 1))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 2-opt breaking the link out of p.
|
||||
if (p + 1 < n) {
|
||||
const double old_link = dist(&exit(p), &entry(p + 1));
|
||||
for (size_t q : neighbors[exit_idx(p)]) {
|
||||
if (! closer(exit(p), q, old_link))
|
||||
break;
|
||||
if (is_exit(q) && pos[q >> 1] > p && try_2opt(p + 1, pos[q >> 1]))
|
||||
return true;
|
||||
}
|
||||
for (size_t q : neighbors[entry_idx(p + 1)]) {
|
||||
if (! closer(entry(p + 1), q, old_link))
|
||||
break;
|
||||
if (! is_exit(q) && pos[q >> 1] <= p && try_2opt(pos[q >> 1], p))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// A run of any length that starts or ends next to the start point moves to the front.
|
||||
if (p == 0)
|
||||
for (size_t q : start_neighbors) {
|
||||
if (q == npos)
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if (m == 0)
|
||||
continue;
|
||||
// Runs m..k enter next to the start, runs k..m leave there and go flipped.
|
||||
const bool exit_near = is_exit(q);
|
||||
for (size_t k = exit_near ? 1 : m; k < (exit_near ? m + 1 : n); ++ k)
|
||||
if (try_oropt(exit_near ? k : m, exit_near ? m : k, size_t(-1)))
|
||||
return true;
|
||||
}
|
||||
// Or-opt of the runs starting at p: next to an exit a run goes after that segment, next to an entry before it.
|
||||
for (size_t last = p; last < std::min(p + 3, n); ++ last) {
|
||||
if (try_oropt(p, last, size_t(-1)))
|
||||
return true;
|
||||
const double removal_gain = dist(prev_exit(p), &entry(p)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(p), next_entry(last));
|
||||
for (size_t e : { entry_idx(p), exit_idx(last) })
|
||||
for (size_t q : neighbors[e]) {
|
||||
if (! closer(end_points[e], q, removal_gain))
|
||||
break;
|
||||
const size_t m = pos[q >> 1];
|
||||
if ((m < p || m > last) && try_oropt(p, last, is_exit(q) ? m : gap_before(m, p, last)))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// A new link can make room for a run that sits elsewhere, hence the sweeps until one finds no move. Every move shortens
|
||||
// the chain, so the search ends; the cap only bounds its time.
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
bool moved_since_sweep = false;
|
||||
for (size_t moves = 0; moves < 50 * n;) {
|
||||
if (queue.empty()) {
|
||||
if (! moved_since_sweep)
|
||||
break;
|
||||
moved_since_sweep = false;
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
enqueue(k);
|
||||
}
|
||||
const size_t segment = queue.front();
|
||||
queue.pop_front();
|
||||
queued[segment] = 0;
|
||||
if (improve_at(pos[segment])) {
|
||||
++ moves;
|
||||
moved_since_sweep = true;
|
||||
enqueue(pos[segment]);
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t k = 0; k < n; ++ k)
|
||||
chain[k] = { order[k], flipped[order[k]] != 0 };
|
||||
}
|
||||
|
||||
// Chain that always goes on to the nearest free end point, from start.
|
||||
static std::vector<std::pair<size_t, bool>> chain_nearest_next(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d &start)
|
||||
{
|
||||
const NearestPoints<1> nearest(end_points);
|
||||
std::vector<char> taken(could_reverse.size(), 0);
|
||||
std::vector<std::pair<size_t, bool>> out;
|
||||
out.reserve(could_reverse.size());
|
||||
for (Vec2d at = start; out.size() < could_reverse.size();) {
|
||||
const size_t e = nearest.query(at, [&](size_t idx) { return ! taken[idx >> 1] && ((idx & 1) == 0 || could_reverse[idx >> 1]); })[0];
|
||||
taken[e >> 1] = 1;
|
||||
out.emplace_back(e >> 1, (e & 1) != 0);
|
||||
at = end_points[e ^ 1];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static double chain_travel(const std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points, const Vec2d &start)
|
||||
{
|
||||
double travel = 0.;
|
||||
Vec2d at = start;
|
||||
for (const std::pair<size_t, bool> &segment : chain) {
|
||||
travel += (end_points[2 * segment.first + segment.second] - at).norm();
|
||||
at = end_points[2 * segment.first + 1 - segment.second];
|
||||
}
|
||||
return travel;
|
||||
}
|
||||
|
||||
template<typename EntityPtr>
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near)
|
||||
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near, bool improve = false)
|
||||
{
|
||||
auto segment_end_point = [&entities](size_t idx, bool first_point) -> Point { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); };
|
||||
auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); };
|
||||
std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near);
|
||||
if (improve && out.size() > 1) {
|
||||
std::vector<Vec2d> end_points;
|
||||
std::vector<char> reversible;
|
||||
end_points.reserve(2 * entities.size());
|
||||
reversible.reserve(entities.size());
|
||||
for (size_t idx = 0; idx < entities.size(); ++ idx) {
|
||||
end_points.emplace_back(segment_end_point(idx, true).template cast<double>());
|
||||
end_points.emplace_back(segment_end_point(idx, false).template cast<double>());
|
||||
reversible.emplace_back(could_reverse(idx));
|
||||
}
|
||||
Vec2d start = Vec2d::Zero();
|
||||
if (start_near != nullptr)
|
||||
start = start_near->cast<double>();
|
||||
if (entities.size() <= 8) {
|
||||
// Few paths are chained exactly; the local search below can stop short of the shortest chain.
|
||||
out = chain_segments_exact(end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
} else {
|
||||
improve_chain_by_local_search(out, end_points, reversible, start_near != nullptr ? &start : nullptr);
|
||||
if (start_near != nullptr) {
|
||||
// The greedy chain can settle into a sweep that local moves cannot undo, which a nearest-next chain may avoid.
|
||||
std::vector<std::pair<size_t, bool>> nearest_next = chain_nearest_next(end_points, reversible, start);
|
||||
if (chain_travel(nearest_next, end_points, start) < chain_travel(out, end_points, start) - SCALED_EPSILON) {
|
||||
improve_chain_by_local_search(nearest_next, end_points, reversible, &start);
|
||||
out = std::move(nearest_next);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (std::pair<size_t, bool> &segment : out) {
|
||||
const ExtrusionEntity *ee = entities[segment.first];
|
||||
if (ee->is_loop())
|
||||
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const s
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near)
|
||||
{
|
||||
chain_and_reorder_extrusion_entities(entities, &start_near);
|
||||
remove_entities_without_endpoints(entities);
|
||||
reorder_extrusion_entities(entities, chain_extrusion_entities_impl(entities, &start_near, true));
|
||||
}
|
||||
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
|
||||
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &e
|
||||
remove_entities_without_endpoints(entities);
|
||||
std::vector<const ExtrusionEntity*> out;
|
||||
out.reserve(entities.size());
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near)) {
|
||||
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near, true)) {
|
||||
if (reverse) {
|
||||
ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get();
|
||||
clone->reverse();
|
||||
@@ -1159,16 +1574,74 @@ std::vector<size_t> chain_points(const Points &points, const Point *start_near)
|
||||
return out;
|
||||
}
|
||||
|
||||
// Moves runs of up to three points of the cycle next to one of the 8 points nearest to an end of the run, reversed or not,
|
||||
// while that shortens the cycle. path[0] stays first. Returns true if any move was made.
|
||||
static bool improve_cycle_by_or_opt(std::vector<size_t> &path, const Points &points)
|
||||
{
|
||||
const size_t pn = path.size();
|
||||
if (pn < 5)
|
||||
return false;
|
||||
std::vector<Vec2d> coords(points.size());
|
||||
for (size_t i = 0; i < points.size(); ++ i)
|
||||
coords[i] = points[i].cast<double>();
|
||||
const NearestPoints<8> grid(coords);
|
||||
std::vector<size_t> pos(points.size());
|
||||
auto refresh = [&]() { for (size_t k = 0; k < pn; ++ k) pos[path[k]] = k; };
|
||||
refresh();
|
||||
auto dist = [&](size_t a, size_t b) { return (coords[path[a % pn]] - coords[path[b % pn]]).norm(); };
|
||||
bool improved = false;
|
||||
for (bool moved = true; moved;) {
|
||||
moved = false;
|
||||
for (size_t len = 1; len <= 3; ++ len)
|
||||
for (size_t i = 1; i + len <= pn; ++ i) {
|
||||
// The run path[i..last] goes between path[k] and path[k + 1].
|
||||
const size_t last = i + len - 1;
|
||||
const double removal = dist(i - 1, i) + dist(last, last + 1) - dist(i - 1, last + 1);
|
||||
bool applied = removal <= SCALED_EPSILON;
|
||||
for (size_t end : { path[i], path[last] })
|
||||
for (size_t q : grid.query(coords[end], [end](size_t idx) { return idx != end; }))
|
||||
for (size_t k : { q == NearestPoints<8>::npos ? pn : (pos[q] + pn - 1) % pn, q == NearestPoints<8>::npos ? pn : pos[q] }) {
|
||||
if (applied || k == pn || (k + 1 >= i && k <= last))
|
||||
continue;
|
||||
const double base = dist(k, k + 1);
|
||||
const double kept = dist(k, i) + dist(last, k + 1) - base;
|
||||
const double reversed = dist(k, last) + dist(i, k + 1) - base;
|
||||
if (std::min(kept, reversed) > removal - SCALED_EPSILON)
|
||||
continue;
|
||||
std::vector<size_t> run(path.begin() + i, path.begin() + last + 1);
|
||||
if (reversed < kept)
|
||||
std::reverse(run.begin(), run.end());
|
||||
path.erase(path.begin() + i, path.begin() + last + 1);
|
||||
path.insert(path.begin() + (k < i ? k + 1 : k + 1 - len), run.begin(), run.end());
|
||||
refresh();
|
||||
applied = moved = improved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return improved;
|
||||
}
|
||||
|
||||
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;
|
||||
std::vector<size_t> path;
|
||||
if (points.size() <= 10) {
|
||||
// Orca: few points are toured exactly, from the point nearest start_near toward its nearer neighbor.
|
||||
path = tsp_exact_cycle(points);
|
||||
if (start_near != nullptr && path.size() > 2) {
|
||||
auto dist = [&points](size_t a, const Point &b) { return (points[a] - b).cast<double>().squaredNorm(); };
|
||||
std::rotate(path.begin(), std::min_element(path.begin(), path.end(), [&](size_t a, size_t b) { return dist(a, *start_near) < dist(b, *start_near); }), path.end());
|
||||
if (dist(path.back(), points[path.front()]) < dist(path[1], points[path.front()]))
|
||||
std::reverse(path.begin() + 1, path.end());
|
||||
}
|
||||
} else {
|
||||
path = chain_points(points, start_near);
|
||||
// Alternate 2-opt, or-opt and crossing removal: each can open moves for the others. Break early if none improves.
|
||||
for (int iter = 0; iter < 3; ++iter) {
|
||||
bool improved = tsp_2opt_improve(path, points);
|
||||
improved |= improve_cycle_by_or_opt(path, points);
|
||||
improved |= tsp_remove_crossings(path, points);
|
||||
if (!improved) break;
|
||||
}
|
||||
}
|
||||
if (start_near == nullptr)
|
||||
tsp_rotate_minimize_closing(path, points);
|
||||
|
||||
@@ -23,9 +23,12 @@ 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);
|
||||
void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain);
|
||||
// From start_near, the greedy chain is refined by 2-opt and or-opt moves, which shortens the travel at some extra cost.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near);
|
||||
// The greedy chain only.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
|
||||
// Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the originals stay unchanged.
|
||||
// Refined as above. Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the
|
||||
// originals stay unchanged.
|
||||
void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near,
|
||||
std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones);
|
||||
|
||||
|
||||
Reference in new issue
Block a user