// Ordering of the tools to minimize tool switches. #ifndef slic3r_ToolOrdering_hpp_ #define slic3r_ToolOrdering_hpp_ #include "../libslic3r.h" #include #include #include #include #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 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, ExtruderPerCopy> entity_map; // to keep track of who prints what // BBS std::map support_map; std::map 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 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 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 components_0based; std::vector 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 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{}(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 per_volume_gradient; }; std::vector 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& 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& 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& 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(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::const_iterator begin() const { return m_layer_tools.begin(); } std::vector::const_iterator end() const { return m_layer_tools.end(); } bool empty() const { return m_layer_tools.empty(); } std::vector& 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. 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>& layer_filaments, const Print* print,const FilamentMapMode mode, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables, const std::map>& unprintable_volumes = {}, const std::unordered_map& 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> nozzle_map_per_layer, const std::vector>& layer_filaments, const std::vector>& layer_sequences, const std::vector& used_filaments, const std::vector>& physical_unprintables, const std::vector>& geometric_unprintables, const std::map>& 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 & initial_non_support_filaments, std::vector & initial_filaments); bool has_non_support_filament(const PrintConfig &config); private: void initialize_layers(std::vector &zs); void collect_extruders(const PrintObject &object, const std::vector> &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 generate_first_layer_tool_order(const Print& print); std::vector generate_first_layer_tool_order(const PrintObject& object); std::vector 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 m_all_printing_extruders; std::vector 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>> 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> 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>> 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 parse_cyclic_order(const std::string& str, unsigned int number_of_extruders); } // namespace SLic3r #endif /* slic3r_ToolOrdering_hpp_ */