mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-23 17:02:39 +00:00
* Toolchange Cyclic Order * Apply cyclic order to first layer * Unit test * Copilot fixes --------- Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
428 lines
23 KiB
C++
428 lines
23 KiB
C++
// Ordering of the tools to minimize tool switches.
|
|
|
|
#ifndef slic3r_ToolOrdering_hpp_
|
|
#define slic3r_ToolOrdering_hpp_
|
|
|
|
#include "../libslic3r.h"
|
|
|
|
#include <functional>
|
|
#include <map>
|
|
#include <utility>
|
|
|
|
#include <boost/container/small_vector.hpp>
|
|
#include "../FilamentGroup.hpp"
|
|
#include "../FilamentMixer.hpp"
|
|
#include "../MultiNozzleUtils.hpp"
|
|
#include "../ExtrusionEntity.hpp"
|
|
#include "../ObjectID.hpp"
|
|
#include "../PrintConfig.hpp"
|
|
|
|
namespace Slic3r {
|
|
|
|
class Print;
|
|
class PrintObject;
|
|
class LayerTools;
|
|
namespace CustomGCode { struct Item; }
|
|
class PrintRegion;
|
|
|
|
// Object of this class holds information about whether an extrusion is printed immediately
|
|
// after a toolchange (as part of infill/perimeter wiping) or not. One extrusion can be a part
|
|
// of several copies - this has to be taken into account.
|
|
class WipingExtrusions
|
|
{
|
|
public:
|
|
bool is_anything_overridden() const { // if there are no overrides, all the agenda can be skipped - this function can tell us if that's the case
|
|
return something_overridden;
|
|
}
|
|
|
|
// When allocating extruder overrides of an object's ExtrusionEntity, overrides for maximum 3 copies are allocated in place.
|
|
typedef boost::container::small_vector<int32_t, 3> ExtruderPerCopy;
|
|
|
|
// This is called from GCode::process_layer - see implementation for further comments:
|
|
const ExtruderPerCopy* get_extruder_overrides(const ExtrusionEntity* entity, const PrintObject* object, int correct_extruder_id, size_t num_of_copies);
|
|
int get_support_extruder_overrides(const PrintObject* object);
|
|
int get_support_interface_extruder_overrides(const PrintObject* object);
|
|
|
|
// This function goes through all infill entities, decides which ones will be used for wiping and
|
|
// marks them by the extruder id. Returns volume that remains to be wiped on the wipe tower:
|
|
float mark_wiping_extrusions(const Print& print, unsigned int old_extruder, unsigned int new_extruder, float volume_to_wipe);
|
|
|
|
void ensure_perimeters_infills_order(const Print& print);
|
|
|
|
bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const;
|
|
bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) {
|
|
bool out = this->is_overriddable(ee, print_config, object, region);
|
|
this->something_overridable |= out;
|
|
return out;
|
|
}
|
|
|
|
// BBS
|
|
bool is_support_overriddable(const ExtrusionRole role, const PrintObject& object) const;
|
|
bool is_support_overriddable_and_mark(const ExtrusionRole role, const PrintObject& object) {
|
|
bool out = this->is_support_overriddable(role, object);
|
|
this->something_overridable |= out;
|
|
return out;
|
|
}
|
|
|
|
bool is_support_overridden(const PrintObject* object) const {
|
|
return support_map.find(object) != support_map.end();
|
|
}
|
|
|
|
bool is_support_interface_overridden(const PrintObject* object) const {
|
|
return support_intf_map.find(object) != support_intf_map.end();
|
|
}
|
|
|
|
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
|
|
|
|
private:
|
|
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
|
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
|
|
|
|
// This function is called from mark_wiping_extrusions and sets extruder that it should be printed with (-1 .. as usual)
|
|
void set_extruder_override(const ExtrusionEntity* entity, const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
|
// BBS
|
|
void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
|
void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies);
|
|
|
|
// Returns true in case that entity is not printed with its usual extruder for a given copy:
|
|
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
|
|
auto it = entity_map.find(std::make_tuple(entity, object));
|
|
return it == entity_map.end() ? false : it->second[copy_id] != -1;
|
|
}
|
|
|
|
std::map<std::tuple<const ExtrusionEntity*, const PrintObject *>, ExtruderPerCopy> entity_map; // to keep track of who prints what
|
|
// BBS
|
|
std::map<const PrintObject*, int> support_map;
|
|
std::map<const PrintObject*, int> support_intf_map;
|
|
bool something_overridable = false;
|
|
bool something_overridden = false;
|
|
const LayerTools* m_layer_tools = nullptr; // so we know which LayerTools object this belongs to
|
|
};
|
|
|
|
|
|
struct FilamentChangeStats
|
|
{
|
|
int filament_flush_weight{0};
|
|
// flush_filament_change_count counts filament changes that actually flush a physical nozzle.
|
|
// It replaces the former (dead, never populated) extruder_change_count. For single-nozzle-per-
|
|
// extruder printers it equals the per-extruder filament_change_count, so GUI stat displays are
|
|
// unchanged.
|
|
int flush_filament_change_count{0};
|
|
int filament_change_count{0};
|
|
|
|
void clear(){
|
|
filament_flush_weight = 0;
|
|
filament_change_count = 0;
|
|
flush_filament_change_count = 0;
|
|
}
|
|
|
|
FilamentChangeStats& operator+=(const FilamentChangeStats& other) {
|
|
this->filament_flush_weight += other.filament_flush_weight;
|
|
this->filament_change_count += other.filament_change_count;
|
|
this->flush_filament_change_count += other.flush_filament_change_count;
|
|
return *this;
|
|
}
|
|
|
|
FilamentChangeStats operator+(const FilamentChangeStats& other){
|
|
FilamentChangeStats ret;
|
|
ret.filament_flush_weight = this->filament_flush_weight + other.filament_flush_weight;
|
|
ret.filament_change_count = this->filament_change_count + other.filament_change_count;
|
|
ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count;
|
|
return ret;
|
|
}
|
|
|
|
};
|
|
|
|
|
|
class LayerTools
|
|
{
|
|
public:
|
|
LayerTools(const coordf_t z) : print_z(z) {}
|
|
|
|
// Changing these operators to epsilon version can make a problem in cases where support and object layers get close to each other.
|
|
// In case someone tries to do it, make sure you know what you're doing and test it properly (slice multiple objects at once with supports).
|
|
bool operator< (const LayerTools &rhs) const { return print_z < rhs.print_z; }
|
|
bool operator==(const LayerTools &rhs) const { return print_z == rhs.print_z; }
|
|
|
|
bool is_extruder_order(unsigned int a, unsigned int b) const;
|
|
bool has_extruder(unsigned int extruder) const { return std::find(this->extruders.begin(), this->extruders.end(), extruder) != this->extruders.end(); }
|
|
|
|
// Return a zero based extruder from the region, or extruder_override if overriden.
|
|
unsigned int wall_extruder_id(const PrintRegion ®ion) const;
|
|
unsigned int sparse_infill_filament_id(const PrintRegion ®ion) const;
|
|
unsigned int internal_solid_filament_id(const PrintRegion ®ion) const;
|
|
// Returns a zero based extruder this eec should be printed with, according to PrintRegion config or extruder_override if overriden.
|
|
unsigned int extruder(const ExtrusionEntityCollection &extrusions, const PrintRegion ®ion) const;
|
|
|
|
coordf_t print_z = 0.;
|
|
bool has_object = false;
|
|
bool has_support = false;
|
|
// Zero based extruder IDs, ordered to minimize tool switches.
|
|
std::vector<unsigned int> extruders;
|
|
// If per layer extruder switches are inserted by the G-code preview slider, this value contains the new (1 based) extruder, with which the whole object layer is being printed with.
|
|
// If not overriden, it is set to 0.
|
|
unsigned int extruder_override = 0;
|
|
// Should a skirt be printed at this layer?
|
|
// Layers are marked for infinite skirt aka draft shield. Not all the layers have to be printed.
|
|
bool has_skirt = false;
|
|
// Will there be anything extruded on this layer for the wipe tower?
|
|
// Due to the support layers possibly interleaving the object layers,
|
|
// wipe tower will be disabled for some support only layers.
|
|
bool has_wipe_tower = false;
|
|
// Number of wipe tower partitions to support the required number of tool switches
|
|
// and to support the wipe tower partitions above this one.
|
|
size_t wipe_tower_partitions = 0;
|
|
coordf_t wipe_tower_layer_height = 0.;
|
|
// Custom G-code (color change, extruder switch, pause) to be performed before this layer starts to print.
|
|
const CustomGCode::Item *custom_gcode = nullptr;
|
|
|
|
// 0-based mixed filament slot → 0-based resolved physical filament for this layer.
|
|
// Populated by ToolOrdering::resolve_mixed_filaments(). Empty when no mixed filaments.
|
|
std::map<unsigned int, unsigned int> mixed_filament_resolution;
|
|
|
|
unsigned int resolve_mixed(unsigned int filament_0based) const {
|
|
auto it = mixed_filament_resolution.find(filament_0based);
|
|
return (it != mixed_filament_resolution.end()) ? it->second : filament_0based;
|
|
}
|
|
|
|
struct MixedSubLayerGroup {
|
|
unsigned int mixed_slot_0based;
|
|
std::vector<unsigned int> components_0based;
|
|
std::vector<double> sub_heights; // per-component, sum ≈ layer_height
|
|
double layer_height = 0.; // the actual lh used to compute sub_heights
|
|
bool is_gradient = false;
|
|
int gradient_first_sorted_idx = 0; // index of "first" config component after sorting
|
|
|
|
struct ObjectGradient {
|
|
size_t total_layers;
|
|
size_t current_idx;
|
|
double gradient_start;
|
|
double gradient_end;
|
|
GradientCurve curve; // empty -> linear fallback (start, end); non-empty wins
|
|
};
|
|
std::map<const PrintObject*, ObjectGradient> per_object_gradient;
|
|
|
|
// Per-volume gradient: same metadata layout as ObjectGradient but keyed by
|
|
// (PrintObject*, ModelVolume id). Populated only when filament_mixed_gradient_per_part is
|
|
// enabled for this slot AND the corresponding ModelObject contains >=2 model-part volumes
|
|
// using this slot. When non-empty for a given (PrintObject*), GCode emission takes the
|
|
// per-volume path for tagged regions; untagged regions (modifier/painted/fuzzy_skin) still
|
|
// use per_object_gradient. Both maps are populated in parallel to keep run states correct.
|
|
struct VolumeKey {
|
|
const PrintObject* obj;
|
|
ObjectID volume_id;
|
|
bool operator<(const VolumeKey &o) const {
|
|
if (obj != o.obj) return std::less<const PrintObject*>{}(obj, o.obj);
|
|
return volume_id < o.volume_id;
|
|
}
|
|
bool operator==(const VolumeKey &o) const {
|
|
return obj == o.obj && volume_id == o.volume_id;
|
|
}
|
|
};
|
|
using VolumeGradient = ObjectGradient;
|
|
std::map<VolumeKey, VolumeGradient> per_volume_gradient;
|
|
};
|
|
std::vector<MixedSubLayerGroup> mixed_sub_layer_groups;
|
|
|
|
const MixedSubLayerGroup* mixed_group_by_slot(unsigned int slot_id) const {
|
|
for (const auto &g : mixed_sub_layer_groups)
|
|
if (g.mixed_slot_0based == slot_id)
|
|
return &g;
|
|
return nullptr;
|
|
}
|
|
|
|
bool is_mixed_slot(unsigned int slot_id) const {
|
|
return mixed_group_by_slot(slot_id) != nullptr;
|
|
}
|
|
|
|
WipingExtrusions& wiping_extrusions() {
|
|
m_wiping_extrusions.set_layer_tools_ptr(this);
|
|
return m_wiping_extrusions;
|
|
}
|
|
|
|
private:
|
|
// This object holds list of extrusion that will be used for extruder wiping
|
|
WipingExtrusions m_wiping_extrusions;
|
|
};
|
|
|
|
class ToolOrdering
|
|
{
|
|
public:
|
|
enum FilamentChangeMode {
|
|
SingleExt,
|
|
MultiExtBest,
|
|
MultiExtCurr
|
|
};
|
|
ToolOrdering() = default;
|
|
|
|
// For the use case when each object is printed separately
|
|
// (print->config().print_sequence == PrintSequence::ByObject is true).
|
|
ToolOrdering(const PrintObject &object, unsigned int first_extruder, bool prime_multi_material = false);
|
|
|
|
// For the use case when all objects are printed at once.
|
|
// (print->config().print_sequence == PrintSequence::ByObject is false).
|
|
ToolOrdering(const Print& print, unsigned int first_extruder, bool prime_multi_material = false);
|
|
|
|
void handle_dontcare_extruder(const std::vector<unsigned int>& first_layer_tool_order);
|
|
void handle_dontcare_extruder(unsigned int first_extruder);
|
|
|
|
void sort_and_build_data(const PrintObject &object, unsigned int first_extruder, bool prime_multi_material = false);
|
|
void sort_and_build_data(const Print& print, unsigned int first_extruder, bool prime_multi_material = false);
|
|
|
|
void clear() {
|
|
m_layer_tools.clear();
|
|
m_stats_by_single_extruder.clear();
|
|
m_stats_by_multi_extruder_best.clear();
|
|
m_stats_by_multi_extruder_curr.clear();
|
|
}
|
|
|
|
// Only valid for non-sequential print:
|
|
// Assign a pointer to a custom G-code to the respective ToolOrdering::LayerTools.
|
|
// Ignore color changes, which are performed on a layer and for such an extruder, that the extruder will not be printing above that layer.
|
|
// If multiple events are planned over a span of a single layer, use the last one.
|
|
void assign_custom_gcodes(const Print &print);
|
|
|
|
// Get the first extruder printing, including the extruder priming areas, returns -1 if there is no layer printed.
|
|
unsigned int first_extruder() const { return m_first_printing_extruder; }
|
|
|
|
// Get the first extruder printing the layer_tools, returns -1 if there is no layer printed.
|
|
unsigned int last_extruder() const { return m_last_printing_extruder; }
|
|
|
|
// For a multi-material print, the printing extruders are ordered in the order they shall be primed.
|
|
const std::vector<unsigned int>& all_extruders() const { return m_all_printing_extruders; }
|
|
// 0-based mixed (virtual) slots that appeared on layers before resolve_mixed_filaments
|
|
// expanded them to physical components.
|
|
const std::vector<unsigned int>& used_mixed_filaments() const { return m_used_mixed_filaments; }
|
|
|
|
// Find LayerTools with the closest print_z.
|
|
const LayerTools& tools_for_layer(coordf_t print_z) const;
|
|
LayerTools& tools_for_layer(coordf_t print_z) { return const_cast<LayerTools&>(std::as_const(*this).tools_for_layer(print_z)); }
|
|
|
|
const LayerTools& front() const { return m_layer_tools.front(); }
|
|
const LayerTools& back() const { return m_layer_tools.back(); }
|
|
std::vector<LayerTools>::const_iterator begin() const { return m_layer_tools.begin(); }
|
|
std::vector<LayerTools>::const_iterator end() const { return m_layer_tools.end(); }
|
|
bool empty() const { return m_layer_tools.empty(); }
|
|
std::vector<LayerTools>& layer_tools() { return m_layer_tools; }
|
|
bool has_wipe_tower() const { return ! m_layer_tools.empty() && m_first_printing_extruder != (unsigned int)-1 && m_layer_tools.front().has_wipe_tower; }
|
|
|
|
int get_most_used_extruder() const { return most_used_extruder; }
|
|
|
|
// Logical (extruder, nozzle) grouping of the used filaments, built during reorder.
|
|
// For single-nozzle printers this is one logical nozzle per extruder (nozzle id == extruder id).
|
|
// Consumed by GCode (get_nozzle_id / get_first_nozzle_for_filament).
|
|
const MultiNozzleUtils::LayeredNozzleGroupResult &get_layered_nozzle_group_result() const { return m_nozzle_group_result; }
|
|
|
|
// Physical nozzle occupancy threading for the sequential (by-object) selector regroup: the
|
|
// setter seeds both the initial recorder (the state the per-layer plan starts from) and the
|
|
// running recorder (read back after sort_and_build_data via get_nozzle_status()), so each
|
|
// object's plan continues from the nozzle state the previous object ended with.
|
|
const MultiNozzleUtils::NozzleStatusRecorder &get_nozzle_status() const { return m_nozzle_status; }
|
|
void set_nozzle_status(const MultiNozzleUtils::NozzleStatusRecorder &status) { m_initial_nozzle_status = status; m_nozzle_status = status; }
|
|
/*
|
|
* called in single extruder mode, the value in map are all 0
|
|
* called in dual extruder mode, the value in map will be 0 or 1
|
|
* 0 based group id
|
|
*/
|
|
// Nozzle-centric grouping. Returns a nozzle-aware LayeredNozzleGroupResult instead of a plain
|
|
// extruder-level std::vector<int>. Callers derive the 0/1-based extruder map via
|
|
// result.get_extruder_map(). unprintable_volumes / nozzle_status default empty for the static
|
|
// path; the per-layer engine supplies non-empty values.
|
|
static MultiNozzleUtils::LayeredNozzleGroupResult get_recommended_filament_maps(const std::vector<std::vector<unsigned int>>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector<std::set<int>>& physical_unprintables, const std::vector<std::set<int>>& geometric_unprintables, const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes = {}, const std::unordered_map<int, int>& nozzle_status = {});
|
|
|
|
// Wrap stitched per-layer filament->nozzle maps from a sequential (by-object) selector regroup
|
|
// into one print-wide result. nozzle_map_per_layer / layer_filaments / layer_sequences are the
|
|
// per-object planned layers concatenated in print order; nozzle_map_per_layer is taken by value
|
|
// and normalized in place. The nozzle list is rebuilt from the print's grouping context. Returns
|
|
// an empty result when the wrap fails. Lives here (not in Print) to reach the file-local
|
|
// grouping-context builder.
|
|
static MultiNozzleUtils::LayeredNozzleGroupResult build_sequential_group_result(
|
|
Print* print,
|
|
std::vector<std::vector<int>> nozzle_map_per_layer,
|
|
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
|
const std::vector<std::vector<unsigned int>>& layer_sequences,
|
|
const std::vector<unsigned int>& used_filaments,
|
|
const std::vector<std::set<int>>& physical_unprintables,
|
|
const std::vector<std::set<int>>& geometric_unprintables,
|
|
const std::map<int, std::set<NozzleVolumeType>>& unprintable_volumes);
|
|
|
|
// should be called after doing reorder
|
|
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
|
|
void cal_most_used_extruder(const PrintConfig &config);
|
|
float cal_max_additional_fan(const PrintConfig &config);
|
|
bool cal_non_support_filaments(const PrintConfig &config,
|
|
unsigned int & first_non_support_filament,
|
|
std::vector<int> & initial_non_support_filaments,
|
|
std::vector<int> & initial_filaments);
|
|
|
|
bool has_non_support_filament(const PrintConfig &config);
|
|
|
|
private:
|
|
void initialize_layers(std::vector<coordf_t> &zs);
|
|
void collect_extruders(const PrintObject &object, const std::vector<std::pair<double, unsigned int>> &per_layer_extruder_switches);
|
|
void fill_wipe_tower_partitions(const PrintConfig &config, coordf_t object_bottom_z, coordf_t max_layer_height);
|
|
bool insert_wipe_tower_extruder();
|
|
void mark_skirt_layers(const PrintConfig &config, coordf_t max_layer_height);
|
|
void collect_extruder_statistics(bool prime_multi_material);
|
|
void reorder_extruders_for_minimum_flush_volume(bool reorder_first_layer);
|
|
void resolve_mixed_filaments(const PrintConfig &config);
|
|
void enforce_mixed_component_order();
|
|
|
|
// BBS
|
|
std::vector<unsigned int> generate_first_layer_tool_order(const Print& print);
|
|
std::vector<unsigned int> generate_first_layer_tool_order(const PrintObject& object);
|
|
|
|
std::vector<LayerTools> m_layer_tools;
|
|
// First printing extruder, including the multi-material priming sequence.
|
|
unsigned int m_first_printing_extruder = (unsigned int)-1;
|
|
// Final printing extruder.
|
|
unsigned int m_last_printing_extruder = (unsigned int)-1;
|
|
// All extruders, which extrude some material over m_layer_tools.
|
|
std::vector<unsigned int> m_all_printing_extruders;
|
|
std::vector<unsigned int> m_used_mixed_filaments;
|
|
const DynamicPrintConfig* m_print_full_config = nullptr;
|
|
const PrintConfig* m_print_config_ptr = nullptr;
|
|
|
|
// Per-object gradient tracking: slot(0-based) -> PrintObject* -> list of layer indices
|
|
// where that object uses the slot. Populated by collect_extruders, consumed by resolve_mixed_filaments.
|
|
std::map<unsigned int, std::map<const PrintObject*, std::vector<size_t>>> m_mixed_object_layers;
|
|
|
|
// All layer indices (in m_layer_tools) where each object has any layer.
|
|
// Used by gradient run detection to distinguish real gaps (object has a layer
|
|
// that doesn't use the slot) from spurious gaps (another object's layer).
|
|
std::map<const PrintObject*, std::vector<size_t>> m_object_all_layer_indices;
|
|
|
|
// Per-volume gradient tracking: slot(0-based) -> (PrintObject*, ModelVolume id) -> list of
|
|
// layer indices where the given volume contributes to the slot. Populated by collect_extruders
|
|
// alongside m_mixed_object_layers when per_part gradient is enabled for the slot AND the
|
|
// ModelObject has >=2 model-part volumes using the slot. Empty for all other configurations,
|
|
// which keeps every legacy per-object code path bit-identical (loops over an empty map are
|
|
// no-ops; downstream emission falls through to the per-object branch).
|
|
std::map<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
|
|
const PrintObject* m_print_object_ptr = nullptr;
|
|
Print* m_print;
|
|
bool m_sorted = false;
|
|
|
|
FilamentChangeStats m_stats_by_single_extruder;
|
|
FilamentChangeStats m_stats_by_multi_extruder_curr;
|
|
FilamentChangeStats m_stats_by_multi_extruder_best;
|
|
MultiNozzleUtils::LayeredNozzleGroupResult m_nozzle_group_result;
|
|
// Physical nozzle occupancy threaded through the per-layer selector regroup.
|
|
// m_initial_nozzle_status seeds the first combo range (empty for a fresh slice — there is no
|
|
// device continuation state); m_nozzle_status carries the running state out of the plan. Inert
|
|
// for every printer except an H2C profile that enables the filament selector (is_dynamic_group_reorder).
|
|
MultiNozzleUtils::NozzleStatusRecorder m_initial_nozzle_status;
|
|
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status;
|
|
|
|
int most_used_extruder;
|
|
};
|
|
|
|
// Parse the user defined cyclic toolchange sequence ("3,2 , 1 , 4") into 0-based filament indices.
|
|
// Out-of-range entries, duplicates and non-numeric tokens are dropped, so a partially valid string
|
|
// still orders the filaments it does name. Exposed for unit testing.
|
|
std::vector<unsigned int> parse_cyclic_order(const std::string& str, unsigned int number_of_extruders);
|
|
|
|
} // namespace SLic3r
|
|
|
|
#endif /* slic3r_ToolOrdering_hpp_ */
|