diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index fad4d4a106..089e56fe87 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -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> 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, std::vector>> 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 &objects_by_extruder = objects_by_extruder_it->second; - std::vector 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 &instances = filament_plan.first; + std::vector nodes; + std::vector 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 &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 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 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 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* 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 visits; + std::vector instance_seen(instances.size(), false); + std::vector 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 &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 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 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 &instances_to_print = filament_to_print_instances[extruder_id]; + auto &filament_plan = filament_to_print_instances[extruder_id]; + std::vector &instances_to_print = filament_plan.first; + const std::vector &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 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 &islands = instance_to_print.object_by_extruder.islands; + std::vector 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 chain = chain_points(island_centroids, this->last_pos_defined() ? &start_near : nullptr); + std::vector 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(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; diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 7f013f2425..fdb6f54861 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -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, std::vector>> + // 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 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, std::vector>> m_ordering_cache; ExtrusionQualityEstimator m_extrusion_quality_estimator; diff --git a/src/libslic3r/GCode/OrderingStrategies.cpp b/src/libslic3r/GCode/OrderingStrategies.cpp index 6df47c084b..2ba9ae5490 100644 --- a/src/libslic3r/GCode/OrderingStrategies.cpp +++ b/src/libslic3r/GCode/OrderingStrategies.cpp @@ -109,17 +109,22 @@ 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; + // 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]]; + 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 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 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 diff --git a/src/libslic3r/GCode/OrderingStrategies.hpp b/src/libslic3r/GCode/OrderingStrategies.hpp index f8e4661253..598eac8130 100644 --- a/src/libslic3r/GCode/OrderingStrategies.hpp +++ b/src/libslic3r/GCode/OrderingStrategies.hpp @@ -136,6 +136,11 @@ std::vector chain_print_object_instances_snake(const Print 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); +// 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 order_points_with_strategy(const Points& points, PrintOrder print_order, const Point* start_near); + #endif // SLIC3R_TEST_HARNESS } // namespace Slic3r diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 823aec11d7..7510fcf17a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -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::get_enum_values(); def->enum_values.push_back("default"); def->enum_values.push_back("as_obj_list");