Cyclic ordering improvement (#14784)

This commit is contained in:
Ian Bassi
2026-07-27 19:58:54 -03:00
committed by GitHub
parent ef7bfeda9c
commit 33dfb66aa5
5 changed files with 259 additions and 51 deletions

View File

@@ -3127,7 +3127,8 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Therefore initialize the printing extruders from there.
this->set_extruders(tool_ordering.all_extruders());
print_object_instances_ordering =
// By default, order object instances using a nearest neighbor search.
// By default, order object instances using nearest-neighbor chaining plus
// 2-opt and crossing-removal post-processing.
(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)
@@ -5969,63 +5970,128 @@ LayerResult GCode::process_layer(
if (m_farthest_point_timelapse.enabled)
compute_farthest_point(layers, most_used_extruder, support_filaments);
std::map<unsigned int, std::vector<InstanceToPrint>> filament_to_print_instances;
// Per filament: instances to print, and the visit sequence over them. Island-level ordering
// may visit an instance more than once per layer; otherwise one visit per instance.
std::map<unsigned int, std::pair<std::vector<InstanceToPrint>, std::vector<InstanceVisit>>> filament_to_print_instances;
{
// Order individual islands rather than whole instances. Off for by-object sequencing,
// sequential printing, and the explicit AsObjectList order, which tour whole instances.
const bool island_level_ordering = print.config().print_sequence != PrintSequence::ByObject &&
single_object_instance_idx == size_t(-1) &&
print.config().print_order != PrintOrder::AsObjectList;
for (unsigned int filament_id : layer_tools.extruders) {
auto objects_by_extruder_it = by_extruder.find(filament_id);
if (objects_by_extruder_it == by_extruder.end()) continue;
auto &filament_plan = filament_to_print_instances[filament_id];
if (!island_level_ordering) {
// One visit per instance, printing all of its islands.
filament_plan.first = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
filament_plan.second.reserve(filament_plan.first.size());
for (size_t i = 0; i < filament_plan.first.size(); ++i)
filament_plan.second.push_back({i, {}, true});
continue;
}
int plate_idx = print.get_plate_index();
Point wt_pos(print.config().wipe_tower_x.get_at(plate_idx), print.config().wipe_tower_y.get_at(plate_idx));
// Build the instances and one tour node per non-empty island (a single node for
// instances without chainable islands). Positions quantized to 1 mm so small
// centroid drift between layers still hits the tour cache below.
std::vector<GCode::ObjectByExtruder> &objects_by_extruder = objects_by_extruder_it->second;
std::vector<const PrintObject *> print_objects;
for (int obj_idx = 0; obj_idx < objects_by_extruder.size(); obj_idx++) {
auto &object_by_extruder = objects_by_extruder[obj_idx];
std::vector<InstanceToPrint> &instances = filament_plan.first;
std::vector<IslandOrderNode> nodes;
std::vector<size_t> node_instances;
auto quantize_to_mm = [](const Point &pt) -> Point {
const coord_t grid = coord_t(scale_(1.));
// Round to the nearest 1 mm symmetrically (integer division truncates toward
// zero, which would make the bucket straddling the origin twice as wide).
auto q = [grid](coord_t v) -> coord_t {
return ((v >= 0 ? v + grid / 2 : v - grid / 2) / grid) * grid;
};
return Point(q(pt.x()), q(pt.y()));
};
for (ObjectByExtruder &object_by_extruder : objects_by_extruder) {
if (object_by_extruder.islands.empty() && (object_by_extruder.support == nullptr || object_by_extruder.support->empty())) continue;
print_objects.push_back(print.get_object(obj_idx));
const size_t layer_id = &object_by_extruder - objects_by_extruder.data();
const PrintObject *print_object = layers[layer_id].original_object;
if (print_object == nullptr)
continue;
const Layer *obj_layer = layers[layer_id].object_layer;
std::vector<ObjectByExtruder::Island> &islands = object_by_extruder.islands;
const bool islands_chainable = obj_layer != nullptr && islands.size() == obj_layer->lslices.size() + 1;
for (size_t instance_id = 0; instance_id < print_object->instances().size(); ++instance_id) {
const size_t instance_idx = instances.size();
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
print_object->instances()[instance_id].model_instance->get_labeled_id());
const Point &shift = print_object->instances()[instance_id].shift;
const size_t first_node = nodes.size();
if (islands_chainable)
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty()) {
nodes.push_back({print_object->id(), instance_id, i,
quantize_to_mm(obj_layer->lslices[i].contour.centroid() + shift)});
node_instances.emplace_back(instance_idx);
}
if (nodes.size() == first_node) {
// No chainable islands: tour the whole instance as one stop.
nodes.push_back({print_object->id(), instance_id, size_t(-1), quantize_to_mm(shift)});
node_instances.emplace_back(instance_idx);
}
}
}
// Build cache key from sorted object IDs.
std::vector<ObjectID> obj_ids;
for (const PrintObject* po : print_objects)
obj_ids.emplace_back(po->id());
std::sort(obj_ids.begin(), obj_ids.end());
// Check cache: reuse ordering if filament + object set unchanged.
// Reuse the cached tour while this filament's island layout is unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
bool cache_hit = (cache_entry.first == obj_ids);
if (!(cache_entry.first == nodes)) {
cache_entry.first = nodes;
Points node_points;
node_points.reserve(nodes.size());
for (const IslandOrderNode &node : nodes)
node_points.emplace_back(node.pos);
std::vector<size_t> tour = order_points_with_strategy(node_points, print.config().print_order, &wt_pos);
// Chained starting near the wipe tower, reversed so the layer ends near it.
std::reverse(tour.begin(), tour.end());
if (!cache_hit) {
// Compute fresh ordering and store in cache.
cache_entry.first = obj_ids;
cache_entry.second =
print.config().print_order == PrintOrder::Snake
? chain_print_object_instances_snake(print_objects, &wt_pos)
: (print.config().print_order == PrintOrder::BestOfStrategies
? chain_print_object_instances_best_of(print_objects, &wt_pos)
: chain_print_object_instances(print_objects, &wt_pos));
}
// Reverse a local copy; keep cached value intact for reuse.
std::vector<const PrintInstance *> new_ordering = cache_entry.second;
std::reverse(new_ordering.begin(), new_ordering.end());
if (print.config().print_sequence == PrintSequence::ByObject) {
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering, single_object_instance_idx);
} else {
// PrintSequence::ByLayer to use global ordering ( per object ordering ) if intra-layer order PrintOrder::AsObjectList is specified while keeping behaviour of PrintSequence::ByLayer
const std::vector<const PrintInstance*>* ordering_for_filament = (print.config().print_order == PrintOrder::AsObjectList && ordering != nullptr) ? ordering: &new_ordering;
filament_to_print_instances[filament_id] = sort_print_object_instances(objects_by_extruder_it->second, layers, ordering_for_filament, single_object_instance_idx);
// Group consecutive tour stops of the same instance into visits.
std::vector<InstanceVisit> visits;
std::vector<bool> instance_seen(instances.size(), false);
std::vector<int> last_visit_of_instance(instances.size(), -1);
for (size_t node_idx : tour) {
const size_t instance_idx = node_instances[node_idx];
if (visits.empty() || visits.back().instance_idx != instance_idx) {
visits.push_back({instance_idx, {}, !instance_seen[instance_idx]});
instance_seen[instance_idx] = true;
}
if (nodes[node_idx].island_idx != size_t(-1))
visits.back().islands.emplace_back(nodes[node_idx].island_idx);
last_visit_of_instance[instance_idx] = int(visits.size()) - 1;
}
// The trailing catch-all island has no geometry to chain by; append it to the
// instance's last visit.
for (size_t i = 0; i < instances.size(); ++i) {
if (last_visit_of_instance[i] < 0)
continue;
InstanceVisit &last_visit = visits[size_t(last_visit_of_instance[i])];
if (last_visit.islands.empty())
// A visit without explicit islands already prints everything.
continue;
std::vector<ObjectByExtruder::Island> &islands = instances[i].object_by_extruder.islands;
if (!islands.back().by_region.empty())
last_visit.islands.emplace_back(islands.size() - 1);
}
cache_entry.second = std::move(visits);
}
filament_plan.second = cache_entry.second;
}
}
std::set<size_t> layer_object_label_ids;
for (auto iter = filament_to_print_instances.begin(); iter != filament_to_print_instances.end(); ++iter) {
for (const InstanceToPrint &instance : iter->second) {
for (const InstanceToPrint &instance : iter->second.first) {
layer_object_label_ids.insert(instance.label_object_id);
}
}
@@ -6095,7 +6161,7 @@ LayerResult GCode::process_layer(
if (print.config().print_sequence == PrintSequence::ByLayer && m_enable_exclude_object && print.config().support_object_skip_flush.value) {
std::vector<size_t> filament_instances_id;
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id]) filament_instances_id.emplace_back(instance.label_object_id);
for (InstanceToPrint &instance : filament_to_print_instances[extruder_id].first) filament_instances_id.emplace_back(instance.label_object_id);
m_filament_instances_code = _encode_label_ids_to_base64(filament_instances_id);
}
@@ -6176,7 +6242,9 @@ LayerResult GCode::process_layer(
if (layer_tools.has_wipe_tower && m_wipe_tower)
m_last_processor_extrusion_role = erWipeTower;
std::vector<InstanceToPrint> &instances_to_print = filament_to_print_instances[extruder_id];
auto &filament_plan = filament_to_print_instances[extruder_id];
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
// We are almost ready to print. However, we must go through all the objects twice to print the overridden extrusions first (infill/perimeter wiping feature):
std::vector<ObjectByExtruder::Island::Region> by_region_per_copy_cache;
@@ -6184,10 +6252,11 @@ LayerResult GCode::process_layer(
if (is_anything_overridden && print_wipe_extrusions == 0)
gcode+="; PURGING FINISHED\n";
for (InstanceToPrint &instance_to_print : instances_to_print) {
for (const InstanceVisit &visit : instance_visits) {
InstanceToPrint &instance_to_print = instances_to_print[visit.instance_idx];
const auto& inst = instance_to_print.print_object.instances()[instance_to_print.instance_id];
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
if (print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer);
}
@@ -6240,7 +6309,7 @@ LayerResult GCode::process_layer(
m_avoid_crossing_perimeters.use_external_mp_once();
m_last_obj_copy = this_object_copy;
this->set_origin(unscale(offset));
if (instance_to_print.object_by_extruder.support != nullptr) {
if (visit.first_visit && instance_to_print.object_by_extruder.support != nullptr) {
m_layer = layers[instance_to_print.layer_id].support_layer;
m_object_layer_over_raft = false;
@@ -6274,9 +6343,42 @@ LayerResult GCode::process_layer(
m_layer = layer_to_print.layer();
m_object_layer_over_raft = object_layer_over_raft;
}
//FIXME order islands?
// Sequential tool path ordering of multiple parts within the same object, aka. perimeter tracking (#5511)
for (ObjectByExtruder::Island &island : instance_to_print.object_by_extruder.islands) {
// Island print order. Use the islands the tour assigned to this visit; if none,
// chain all islands nearest-neighbor from the current nozzle position (last_pos(),
// in this instance's frame after set_origin() above). Empty islands are skipped;
// the trailing catch-all island has no centroid to chain by and always goes last.
std::vector<ObjectByExtruder::Island> &islands = instance_to_print.object_by_extruder.islands;
std::vector<size_t> island_order = visit.islands;
if (island_order.empty()) {
island_order.reserve(islands.size());
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
for (size_t i = 0; i + 1 < islands.size(); ++i)
if (!islands[i].by_region.empty())
island_order.emplace_back(i);
if (island_order.size() > 1) {
Points island_centroids;
island_centroids.reserve(island_order.size());
for (size_t i : island_order)
island_centroids.emplace_back(layer_to_print.object_layer->lslices[i].contour.centroid());
const Point start_near = this->last_pos();
std::vector<size_t> chain = chain_points(island_centroids, this->last_pos_defined() ? &start_near : nullptr);
std::vector<size_t> ordered;
ordered.reserve(island_order.size());
for (size_t k : chain)
ordered.emplace_back(island_order[k]);
island_order = std::move(ordered);
}
if (!islands.back().by_region.empty())
island_order.emplace_back(islands.size() - 1);
} else {
// Unexpected islands layout, keep the stored order.
for (size_t i = 0; i < islands.size(); ++i)
island_order.emplace_back(i);
}
}
for (size_t island_idx : island_order) {
ObjectByExtruder::Island &island = islands[island_idx];
const auto& by_region_specific = is_anything_overridden ? island.by_region_per_copy(by_region_per_copy_cache, static_cast<unsigned int>(instance_to_print.instance_id), extruder_id, print_wipe_extrusions != 0) : island.by_region;
// When starting a new object, use the external motion planner for the first travel move.
const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift;

View File

@@ -539,9 +539,38 @@ private:
// Cache for custom seam enforcers/blockers for each layer.
SeamPlacer m_seam_placer;
// Cache per-filament ordering to avoid recomputing when object set is unchanged across layers.
// Key: filament_id. Value: {sorted ObjectIDs of objects present on this filament, cached ordering}.
std::map<unsigned int, std::pair<std::vector<ObjectID>, std::vector<const PrintInstance*>>>
// One stop of the island-level tour: consecutive islands of a single instance. An instance
// can have several visits per layer when its islands are toured non-consecutively.
struct InstanceVisit
{
// Index into the per-filament InstanceToPrint vector.
size_t instance_idx;
// Islands to print, in order (indices into ObjectByExtruder::islands). Empty: print all
// islands, ordered at extrusion time.
std::vector<size_t> islands;
// First visit of this instance this layer; skirt, brim and support are emitted here.
bool first_visit;
};
// One node of the island-level tour, also used as cache key: identity plus quantized position.
struct IslandOrderNode
{
ObjectID object_id;
size_t instance_id;
// Index into ObjectByExtruder::islands, or size_t(-1) for an instance without chainable
// islands (e.g. support only), which is toured as a single stop.
size_t island_idx;
// Island centroid in G-code coordinates, quantized to 1 mm for cache stability.
Point pos;
bool operator==(const IslandOrderNode &rhs) const {
return object_id == rhs.object_id && instance_id == rhs.instance_id &&
island_idx == rhs.island_idx && pos == rhs.pos;
}
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;

View File

@@ -109,17 +109,22 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
size_t pn = path.size();
if (pn <= 3) return false;
size_t n_edges = pn - 1;
// 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<size_t, size_t> {
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[i + 1]];
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]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
@@ -374,4 +379,57 @@ std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Pri
return chain_print_object_instances_best_of(print.objects().vector(), nullptr);
}
/* ====================================================================
* Island-level ordering entry point
* ==================================================================== */
std::vector<size_t> 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<size_t> 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<double>::max();
for (size_t k = 0; k < points.size(); ++k) {
double d2 = (points[k].cast<double>() - start_near->cast<double>()).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<std::vector<size_t>> 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<double>::max();
double best_edge = std::numeric_limits<double>::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

View File

@@ -136,6 +136,11 @@ std::vector<const PrintInstance*> chain_print_object_instances_snake(const Print
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const std::vector<const PrintObject*>& print_objects, const Point* start_near);
std::vector<const PrintInstance*> chain_print_object_instances_best_of(const Print& print);
// Order raw points with the selected strategy, returning an index permutation. Island-level
// counterpart of the chain_print_object_instances_* helpers. The returned cycle starts at the
// point closest to start_near; orders without a dedicated strategy use nearest-neighbor chaining.
std::vector<size_t> order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near);
#endif // SLIC3R_TEST_HARNESS
} // namespace Slic3r

View File

@@ -2001,7 +2001,21 @@ void PrintConfigDef::init_fff_params()
def = this->add("print_order", coEnum);
def->label = L("Intra-layer order");
def->tooltip = L("Print order within a single layer.");
def->tooltip = L("Order in which object instances are visited within a single layer, which controls how much "
"travel is spent moving between them.\n\n"
"Default: nearest-neighbor chaining, refined with 2-opt and crossing removal. A good general "
"choice.\n"
"As object list: instances are printed in the same order as the object list, without any path "
"optimization. Use it when you need a predictable, manually controlled order.\n"
"Best of all (shortest path): every strategy is evaluated and the shortest one is used. The "
"object instance order is decided once for the whole print, while the ordering of individual "
"islands is decided per layer, so different layers may end up using different strategies. "
"Slightly slower to slice.\n"
"Snake: serpentine row-by-row traversal, refined with 2-opt. Well suited to regular grids of "
"many small parts.\n\n"
"With multiple filaments or tools in the same layer, minimizing tool changes takes priority: "
"objects are grouped by filament first and this setting only orders the instances within each "
"filament group, so the overall sequence may not look like the shortest path across the plate.");
def->enum_keys_map = &ConfigOptionEnum<PrintOrder>::get_enum_values();
def->enum_values.push_back("default");
def->enum_values.push_back("as_obj_list");