mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-05 09:07:39 +00:00
feat(libslic3r): multi-nozzle slicing engine for H2C/A2L
Port BambuStudio's dual-nozzle slicing core: H2C-era config keys, filament-to-nozzle grouping with per-layer dynamic regrouping, filament/nozzle/hotend gcode placeholder vocabulary, multi-nozzle wipe tower pre-heat/pre-cool, the two-pass pre-cooling injector, and corexy farthest-point timelapse.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/CustomGCode.hpp"
|
||||
#include "libslic3r/MultiNozzleUtils.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
@@ -43,6 +44,23 @@ class Print;
|
||||
Count
|
||||
};
|
||||
|
||||
// Classifies why a wipe-tower / change_filament / time-lapse region is safe to relocate a
|
||||
// pre-heat M104 into, for the pre-heat/pre-cool injector. The shipping time_lapse_gcode
|
||||
// template (timelapse-on by default) emits SKIPPABLE_* on essentially every slice, so the
|
||||
// "timelapse" payload -> stTimelapse classification is exercised widely.
|
||||
enum SkipType
|
||||
{
|
||||
stTimelapse,
|
||||
stHeadWrapDetect,
|
||||
stOther,
|
||||
stNone
|
||||
};
|
||||
|
||||
const std::unordered_map<std::string_view, SkipType> skip_type_map{
|
||||
{"timelapse", SkipType::stTimelapse},
|
||||
{"head_wrap_detect", SkipType::stHeadWrapDetect}
|
||||
};
|
||||
|
||||
struct PrintEstimatedStatistics
|
||||
{
|
||||
enum class ETimeMode : unsigned char
|
||||
@@ -77,6 +95,10 @@ class Print;
|
||||
|
||||
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
|
||||
unsigned int total_filament_changes;
|
||||
// Number of filament changes that actually re-flush a nozzle (a filament-in-nozzle change
|
||||
// onto a non-empty nozzle), tracked only by the richer multi-nozzle hotend-change time model.
|
||||
// Stays 0 for single-nozzle printers (X1/P1/A1/H2S/A2L), which never enter the two-arg model.
|
||||
unsigned int total_flush_filament_changes;
|
||||
unsigned int total_extruder_changes;
|
||||
float total_filament_load_time;
|
||||
float total_filament_unload_time;
|
||||
@@ -101,6 +123,7 @@ class Print;
|
||||
flush_per_filament.clear();
|
||||
used_filaments_per_role.clear();
|
||||
total_filament_changes = 0;
|
||||
total_flush_filament_changes = 0;
|
||||
total_extruder_changes = 0;
|
||||
total_filament_load_time = 0.0f;
|
||||
total_filament_unload_time = 0.0f;
|
||||
@@ -166,6 +189,14 @@ class Print;
|
||||
ConflictResultOpt conflict_result;
|
||||
GCodeCheckResult gcode_check_result;
|
||||
FilamentPrintableResult filament_printable_reuslt;
|
||||
// The per-filament -> logical-nozzle grouping the slicer computed for this
|
||||
// result, surfaced onto the object the device GUI reads
|
||||
// (plater->background_process().get_current_gcode_result()). Populated only from
|
||||
// Print::get_layered_nozzle_group_result() (ToolOrdering's static L/R + rack subset);
|
||||
// default-empty (null) and read by no g-code emitter, so it is invisible in the emitted
|
||||
// g-code. Consumed by the print-dispatch nozzle mapping (DevNozzleMappingCtrl) via
|
||||
// DevUtilBackend::GetNozzleGroupResult.
|
||||
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> nozzle_group_result;
|
||||
float initial_layer_time;
|
||||
|
||||
struct SettingsIds
|
||||
@@ -263,6 +294,10 @@ class Print;
|
||||
std::vector<SliceWarning> warnings;
|
||||
int nozzle_hrc;
|
||||
std::vector<NozzleType> nozzle_type;
|
||||
// Per-extruder physical hotend type. Fed to the pre-heat injector's TimeProcessContext
|
||||
// (mixed-type X2D workaround). Populated in apply_config; unused until the injector side-pass
|
||||
// consumes it.
|
||||
std::vector<ExtruderType> extruder_types;
|
||||
// first key stores filaments, second keys stores the layer ranges(enclosed) that use the filaments
|
||||
std::unordered_map<std::vector<unsigned int>, std::vector<std::pair<int, int>>,FilamentSequenceHash> layer_filaments;
|
||||
std::vector<unsigned int> nozzle_change_sequence;
|
||||
@@ -271,6 +306,11 @@ class Print;
|
||||
// first key stores `from` filament, second keys stores the `to` filament
|
||||
std::map<std::pair<int,int>, int > filament_change_count_map;
|
||||
|
||||
// Accumulated print time spent inside SKIPPABLE regions, per skip type. Populated by the time
|
||||
// estimator; consumed only downstream. The shipping time_lapse_gcode template emits SKIPPABLE_*
|
||||
// widely, so this is typically populated (stTimelapse) on most slices.
|
||||
std::unordered_map<SkipType, float> skippable_part_time;
|
||||
|
||||
BedType bed_type = BedType::btCount;
|
||||
void reset();
|
||||
|
||||
@@ -304,11 +344,18 @@ class Print;
|
||||
gcode_check_result = other.gcode_check_result;
|
||||
limit_filament_maps = other.limit_filament_maps;
|
||||
filament_printable_reuslt = other.filament_printable_reuslt;
|
||||
// Orca: copy the shared grouping result so a copied result keeps it (shared_ptr =>
|
||||
// memory-safe), rather than leaving a stale pointer on the target. No g-code effect either way.
|
||||
nozzle_group_result = other.nozzle_group_result;
|
||||
// Keep the per-extruder hotend types on a copied result (injector input).
|
||||
extruder_types = other.extruder_types;
|
||||
layer_filaments = other.layer_filaments;
|
||||
filament_change_sequence = other.filament_change_sequence;
|
||||
nozzle_change_sequence = other.nozzle_change_sequence;
|
||||
optimal_assignment = other.optimal_assignment;
|
||||
filament_change_count_map = other.filament_change_count_map;
|
||||
// Keep the SKIPPABLE per-type time on a copied result.
|
||||
skippable_part_time = other.skippable_part_time;
|
||||
initial_layer_time = other.initial_layer_time;
|
||||
#if ENABLE_GCODE_VIEWER_STATISTICS
|
||||
time = other.time;
|
||||
@@ -319,6 +366,75 @@ class Print;
|
||||
void unlock() const { result_mutex.unlock(); }
|
||||
};
|
||||
|
||||
// First-pass usage-block descriptors for the pre-heat/pre-cool injector. FilamentUsageBlock
|
||||
// records the [lower,upper) output-line-id span a single filament occupies; ExtruderUsageBlcok
|
||||
// (the "Blcok" typo is intentional) records the span an extruder is active in, with the start/end
|
||||
// filament + logical-nozzle ids and the post-extrusion (pre-switch) partial-free sub-range. Built
|
||||
// during run_post_process, consumed only by the injector side-pass under the enable_pre_heating gate.
|
||||
namespace ExtruderPreHeating
|
||||
{
|
||||
struct FilamentUsageBlock
|
||||
{
|
||||
int filament_id;
|
||||
int extruder_id;
|
||||
int nozzle_id;
|
||||
unsigned int lower_gcode_id;
|
||||
unsigned int upper_gcode_id; // [lower_gcode_id,upper_gcode_id) uses current filament , upper gcode id will be set after finding next block
|
||||
FilamentUsageBlock(int filament_id_, int extruder_id_, int nozzle_id_, unsigned int lower_gcode_id_, unsigned int upper_gcode_id_) :filament_id(filament_id_), extruder_id(extruder_id_), nozzle_id(nozzle_id_), lower_gcode_id(lower_gcode_id_), upper_gcode_id(upper_gcode_id_) {}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Describle the usage of a exturder in a section
|
||||
*
|
||||
* The strucutre stores the start and end lines of the sections as well as
|
||||
* the filament used at the beginning and end of the section.
|
||||
* Post extrusion means the final extrusion before switching to the next extruder.
|
||||
*
|
||||
* Simplified GCode Flow:
|
||||
* 1.Extruder Change Block (ext0 switch to ext1)
|
||||
* 2.Extruder Usage Block (use ext1 to print)
|
||||
* 3.Extruder Change Block (ext1 switch to ext0)
|
||||
* 4.Extruder Usage Block (use ext0 to print)
|
||||
* 5.Extruder Change Block (ext0 switch to ex1)
|
||||
* ...
|
||||
*
|
||||
* So the construct of extruder usage block relys on two extruder change block
|
||||
*/
|
||||
struct ExtruderUsageBlcok
|
||||
{
|
||||
int extruder_id = -1;
|
||||
unsigned int start_id = -1;
|
||||
unsigned int end_id = -1;
|
||||
int start_filament = -1;
|
||||
int end_filament = -1;
|
||||
int start_nozzle_id = -1;
|
||||
int end_nozzle_id = -1;
|
||||
unsigned int post_extrusion_start_id = -1;
|
||||
unsigned int post_extrusion_end_id = -1;
|
||||
bool ignore_cooling_before_tower = false;
|
||||
|
||||
void initialize_step_1(int extruder_id_, int start_id_, int start_filament_, int start_nozzle_id_) {
|
||||
extruder_id = extruder_id_;
|
||||
start_id = start_id_;
|
||||
start_filament = start_filament_;
|
||||
start_nozzle_id = start_nozzle_id_;
|
||||
};
|
||||
void initialize_step_2(int post_extrusion_start_id_) {
|
||||
post_extrusion_start_id = post_extrusion_start_id_;
|
||||
}
|
||||
void initialize_step_3(int end_id_, int end_filament_, int post_extrusion_end_id_, int end_nozzle_id_) {
|
||||
end_id = end_id_;
|
||||
end_filament = end_filament_;
|
||||
post_extrusion_end_id = post_extrusion_end_id_;
|
||||
end_nozzle_id = end_nozzle_id_;
|
||||
}
|
||||
void reset() {
|
||||
*this = ExtruderUsageBlcok();
|
||||
}
|
||||
ExtruderUsageBlcok() = default;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
class CommandProcessor {
|
||||
public:
|
||||
@@ -347,6 +463,24 @@ class Print;
|
||||
static const std::string VFlush_Start_Tag;
|
||||
static const std::string VFlush_End_Tag;
|
||||
static const std::string External_Purge_Tag;
|
||||
public:
|
||||
// Orca: SKIPPABLE region tags, stored as static strings (the FLUSH idiom above) rather than
|
||||
// a CustomETags/CustomTags array. Public so the emission sites (WipeTower / change_filament
|
||||
// path) can reference them single-sourced.
|
||||
static const std::string Skippable_Start_Tag;
|
||||
static const std::string Skippable_End_Tag;
|
||||
static const std::string Skippable_Type_Tag;
|
||||
// Orca: usage-block builder markers (MACHINE_START_GCODE_END / MACHINE_END_GCODE_START /
|
||||
// NOZZLE_CHANGE_START / NOZZLE_CHANGE_END / CP_TOOLCHANGE_WIPE), stored as static strings (the
|
||||
// FLUSH/SKIPPABLE idiom above) rather than extending the Reserved_Tags arrays — these are
|
||||
// multi-nozzle markers only ever emitted by BBL-printer paths. Public so the emission sites can
|
||||
// reference them single-sourced. The MACHINE_*_GCODE_* emission (GCode.cpp, gated
|
||||
// enable_pre_heating) activates the usage-block builder.
|
||||
static const std::string Machine_Start_GCode_End_Tag;
|
||||
static const std::string Machine_End_GCode_Start_Tag;
|
||||
static const std::string Nozzle_Change_Start_Tag;
|
||||
static const std::string Nozzle_Change_End_Tag;
|
||||
static const std::string Toolchange_Wipe_Tag;
|
||||
public:
|
||||
enum class ETags : unsigned char
|
||||
{
|
||||
@@ -455,6 +589,9 @@ class Print;
|
||||
|
||||
EMoveType move_type{ EMoveType::Noop };
|
||||
ExtrusionRole role{ erNone };
|
||||
// SKIPPABLE tag classification stamped onto each time block. Feeds skippable_part_time
|
||||
// and the injector's SKIPPABLE relocation. stNone unless inside a SKIPPABLE_* region.
|
||||
SkipType skippable_type{ SkipType::stNone };
|
||||
unsigned int move_id{ 0 };
|
||||
unsigned int g1_line_id{ 0 };
|
||||
unsigned int remaining_internal_g1_lines{ 0 };
|
||||
@@ -624,6 +761,25 @@ class Print;
|
||||
|
||||
struct TimeProcessor
|
||||
{
|
||||
// Orca: the insert-line taxonomy + the ordered map of lines the pre-heat/pre-cool injector
|
||||
// splices into the finished g-code, keyed by output-line id. Orca keeps its single-pass
|
||||
// run_post_process (M73 / filament stats / ActualSpeedMove / Backtrace /
|
||||
// machine_tool_change_time) intact and applies this map in a separate, gated ADDITIVE
|
||||
// second file-rewrite pass (run_second_pass_injection); with an empty map that pass is a
|
||||
// byte-for-byte identity rewrite. The map is populated by the PreCoolingInjector.
|
||||
enum InsertLineType
|
||||
{
|
||||
PlaceholderReplace,
|
||||
TimePredict,
|
||||
FilamentChangePredict,
|
||||
ExtruderChangePredict,
|
||||
PreCooling,
|
||||
PreHeating,
|
||||
};
|
||||
|
||||
// first key is line id, second key is content
|
||||
using InsertedLinesMap = std::map<unsigned int, std::vector<std::pair<std::string, InsertLineType>>>;
|
||||
|
||||
struct Planner
|
||||
{
|
||||
// Size of the firmware planner queue. The old 8-bit Marlins usually just managed 16 trapezoidal blocks.
|
||||
@@ -651,6 +807,117 @@ class Print;
|
||||
|
||||
void reset();
|
||||
};
|
||||
|
||||
// The pre-cool / pre-heat injection engine. It consumes the already-computed per-move time
|
||||
// substrate (moves[i].time[valid_machine_id] / .gcode_id) and the first-pass usage blocks to
|
||||
// locate idle-hotend windows, then emits M632/M400/M104/M633 lines into a
|
||||
// TimeProcessor::InsertedLinesMap that the additive second file-rewrite pass
|
||||
// (run_second_pass_injection) splices into the finished g-code. It is constructed and run ONLY
|
||||
// when m_enable_pre_heating — single-nozzle printers (X1/P1/A1/H2S, flag false) never reach it.
|
||||
// Every input is a const reference bundled from GCodeProcessor members; the injector never
|
||||
// mutates GCodeProcessor state.
|
||||
class PreCoolingInjector {
|
||||
public:
|
||||
struct ExtruderFreeBlock {
|
||||
unsigned int free_lower_gcode_id;
|
||||
unsigned int free_upper_gcode_id;
|
||||
unsigned int partial_free_lower_id; // range of extrusion in wipe tower; without a wipe tower
|
||||
unsigned int partial_free_upper_id; // partial_free lower/upper equal free_lower_gcode_id
|
||||
int last_filament_id;
|
||||
int next_filament_id;
|
||||
int last_nozzle_id;
|
||||
int next_nozzle_id;
|
||||
int extruder_id; // partition key for the pre-heat/pre-cool region (extruder or hotend), not
|
||||
// necessarily a real extruder id
|
||||
bool ignore_cooling_before_tower = false;
|
||||
};
|
||||
|
||||
void process_pre_cooling_and_heating(TimeProcessor::InsertedLinesMap& inserted_operation_lines);
|
||||
void build_extruder_free_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks, const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
|
||||
|
||||
PreCoolingInjector(
|
||||
const std::vector<GCodeProcessorResult::MoveVertex>& moves_,
|
||||
const std::vector<std::string>& filament_types_,
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result_,
|
||||
const std::vector<int>& filament_nozzle_temps_,
|
||||
const std::vector<int>& filament_nozzle_temps_initial_layer_,
|
||||
const std::vector<int>& physical_extruder_map_,
|
||||
int valid_machine_id_,
|
||||
float inject_time_threshold_,
|
||||
bool handle_hotend_as_extruder_,
|
||||
bool has_filament_switcher_,
|
||||
const std::vector<int>& pre_cooling_temp_,
|
||||
const std::vector<double>& cooling_rate_,
|
||||
const std::vector<double>& heating_rate_,
|
||||
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks_,
|
||||
const std::vector<int>& extruder_max_nozzle_count_,
|
||||
const std::vector<double>& filament_preheat_temperature_delta_,
|
||||
const std::vector<double>& filament_max_temperature_drop_when_ec_,
|
||||
unsigned int machine_start_gcode_end_id_,
|
||||
unsigned int machine_end_gcode_start_id_,
|
||||
const std::vector<ExtruderType>& extruder_types_,
|
||||
const std::vector<double>& nozzle_diameter_
|
||||
) :
|
||||
moves(moves_),
|
||||
filament_types(filament_types_),
|
||||
nozzle_group_result(nozzle_group_result_),
|
||||
filament_nozzle_temps(filament_nozzle_temps_),
|
||||
filament_nozzle_temps_initial_layer(filament_nozzle_temps_initial_layer_),
|
||||
physical_extruder_map(physical_extruder_map_),
|
||||
valid_machine_id(valid_machine_id_),
|
||||
inject_time_threshold(inject_time_threshold_),
|
||||
handle_hotend_as_extruder(handle_hotend_as_extruder_),
|
||||
has_filament_switcher(has_filament_switcher_),
|
||||
filament_pre_cooling_temps(pre_cooling_temp_),
|
||||
cooling_rate(cooling_rate_),
|
||||
heating_rate(heating_rate_),
|
||||
skippable_blocks(skippable_blocks_),
|
||||
extruder_max_nozzle_count(extruder_max_nozzle_count_),
|
||||
filament_preheat_temperature_delta(filament_preheat_temperature_delta_),
|
||||
filament_max_temperature_drop_when_ec(filament_max_temperature_drop_when_ec_),
|
||||
machine_start_gcode_end_id(machine_start_gcode_end_id_),
|
||||
machine_end_gcode_start_id(machine_end_gcode_start_id_),
|
||||
extruder_types(extruder_types_),
|
||||
nozzle_diameter(nozzle_diameter_)
|
||||
{
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<ExtruderFreeBlock> m_extruder_free_blocks;
|
||||
const std::vector<GCodeProcessorResult::MoveVertex>& moves;
|
||||
const std::vector<std::string>& filament_types;
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result;
|
||||
const std::vector<int>& filament_nozzle_temps;
|
||||
const std::vector<int>& filament_nozzle_temps_initial_layer;
|
||||
const std::vector<int>& physical_extruder_map;
|
||||
const int valid_machine_id;
|
||||
const float inject_time_threshold;
|
||||
const bool handle_hotend_as_extruder;
|
||||
const bool has_filament_switcher;
|
||||
const std::vector<double>& cooling_rate;
|
||||
const std::vector<double>& heating_rate;
|
||||
const std::vector<int>& filament_pre_cooling_temps; // target cooling temp during post extrusion
|
||||
const std::vector<std::pair<unsigned int, unsigned int>>& skippable_blocks;
|
||||
const std::vector<int>& extruder_max_nozzle_count;
|
||||
const std::vector<double>& filament_preheat_temperature_delta;
|
||||
const std::vector<double>& filament_max_temperature_drop_when_ec;
|
||||
const unsigned int machine_start_gcode_end_id;
|
||||
const unsigned int machine_end_gcode_start_id;
|
||||
const std::vector<ExtruderType>& extruder_types;
|
||||
const std::vector<double>& nozzle_diameter;
|
||||
|
||||
void inject_cooling_heating_command(
|
||||
TimeProcessor::InsertedLinesMap& inserted_operation_lines,
|
||||
const ExtruderFreeBlock& free_block,
|
||||
float curr_temp,
|
||||
float target_temp,
|
||||
bool pre_cooling,
|
||||
bool pre_heating
|
||||
);
|
||||
|
||||
void build_by_filament_blocks(const std::vector<ExtruderPreHeating::FilamentUsageBlock>& filament_usage_blocks);
|
||||
void build_by_extruder_blocks(const std::vector<ExtruderPreHeating::ExtruderUsageBlcok>& extruder_usage_blocks);
|
||||
};
|
||||
public:
|
||||
class SeamsDetector
|
||||
{
|
||||
@@ -795,12 +1062,51 @@ class Print;
|
||||
bool m_flushing; // mark a section with real flush
|
||||
bool m_virtual_flushing; // mark a section with virtual flush, only for statistics
|
||||
bool m_wipe_tower;
|
||||
// Current-section SKIPPABLE state. Set by process_tags when inside a SKIPPABLE_* region;
|
||||
// stamped onto each TimeBlock. The shipping time_lapse_gcode template emits SKIPPABLE_*
|
||||
// widely, so these commonly go active (true / stTimelapse) and stamp blocks on most slices.
|
||||
bool m_skippable{false};
|
||||
SkipType m_skippable_type{SkipType::stNone};
|
||||
int m_object_label_id{-1};
|
||||
float m_print_z{0.0f};
|
||||
std::vector<float> m_remaining_volume;
|
||||
ExtruderTemps m_filament_nozzle_temp;
|
||||
ExtruderTemps m_filament_nozzle_temp_first_layer;
|
||||
std::vector<int> m_physical_extruder_map;
|
||||
// Multi-nozzle context state. Per-extruder max (sub-)nozzle count; >1 marks a multi-nozzle
|
||||
// extruder. Input for the pre-heat/filament-change-time injection model; not yet consumed by
|
||||
// Orca's time estimator, so it is inert for existing printers.
|
||||
std::vector<int> m_extruder_max_nozzle_count{1};
|
||||
// Pre-heat / pre-cool injector estimator inputs. Populated from the config in apply_config
|
||||
// (both overloads) and cleared in reset(), so the PreCoolingInjector has its inputs in place.
|
||||
// Consumed only by the injector two-pass side-pass, gated on m_enable_pre_heating.
|
||||
std::vector<std::string> m_filament_types;
|
||||
std::vector<double> m_nozzle_diameter;
|
||||
std::vector<double> m_hotend_cooling_rate{ 2.f };
|
||||
std::vector<double> m_hotend_heating_rate{ 2.f };
|
||||
std::vector<int> m_filament_pre_cooling_temp{ 0 };
|
||||
std::vector<double> m_filament_preheat_temperature_delta;
|
||||
bool m_enable_pre_heating{ false };
|
||||
bool m_handle_hotend_as_extruder{ false };
|
||||
bool m_has_filament_switcher{ false };
|
||||
// [start,end] output-line-id ranges of each SKIPPABLE region, collected during
|
||||
// run_post_process. The injector relocates pre-heat M104s out of these ranges. The shipping
|
||||
// time_lapse_gcode template emits SKIPPABLE_* widely, so on a timelapse-on slice this is
|
||||
// populated with many timelapse ranges (not empty) — the consumer must expect the common
|
||||
// timelapse case, not only H2C/A2L wipe-tower ranges.
|
||||
std::vector<std::pair<unsigned int, unsigned int>> m_skippable_blocks;
|
||||
// First-pass usage blocks, built in run_post_process and stored on the member so the
|
||||
// injector side-pass can consume them. Filled only when m_enable_pre_heating — single-nozzle
|
||||
// printers (X1/P1/A1/H2S) never build them. They depend on the MACHINE_*_GCODE_* /
|
||||
// NOZZLE_CHANGE_* emission the builder keys off.
|
||||
std::vector<ExtruderPreHeating::FilamentUsageBlock> m_filament_blocks;
|
||||
std::vector<ExtruderPreHeating::ExtruderUsageBlcok> m_extruder_blocks;
|
||||
unsigned int m_machine_start_gcode_end_line_id{ (unsigned int) (-1) };
|
||||
unsigned int m_machine_end_gcode_start_line_id{ (unsigned int) (-1) };
|
||||
// Tracks, during the stream, which filament sits in each physical nozzle and which nozzle each
|
||||
// extruder currently carries. Consumed ONLY by the richer two-arg process_filament_change
|
||||
// model, which single-nozzle printers (X1/P1/A1/H2S/A2L) never enter.
|
||||
MultiNozzleUtils::NozzleStatusRecorder m_nozzle_status_recorder;
|
||||
bool m_manual_filament_change;
|
||||
|
||||
//BBS: x, y offset for gcode generated
|
||||
@@ -1094,22 +1400,43 @@ class Print;
|
||||
// Processes T line (Select Tool)
|
||||
void process_T(const GCodeReader::GCodeLine& line);
|
||||
void process_T(const std::string_view command);
|
||||
// T variant carrying the H<nozzle> logical-nozzle id parsed off the command line. -1 = absent.
|
||||
void process_T(const std::string_view command, int nozzle_id);
|
||||
void process_M1020(const GCodeReader::GCodeLine &line);
|
||||
|
||||
void process_M622(const GCodeReader::GCodeLine &line);
|
||||
void process_M623(const GCodeReader::GCodeLine &line);
|
||||
|
||||
void process_filament_change(int id);
|
||||
// Richer hotend-change time model distinguishing extruder-switch / nozzle-in-extruder change /
|
||||
// filament-in-nozzle change. Self-gated: for single-nozzle printers it delegates to
|
||||
// process_filament_change(int) so their time estimate — hence exported g-code — is unchanged.
|
||||
void process_filament_change(int id, int nozzle_id);
|
||||
// True only for multi-nozzle-capable printers (H2C cluster, or a dual/multi-extruder machine
|
||||
// like H2D/X2D): the gate that admits the richer two-arg hotend-change time model. False for
|
||||
// every single-extruder single-nozzle printer (X1/P1/A1/H2S/A2L).
|
||||
bool use_multi_nozzle_change_time_model() const;
|
||||
|
||||
// post process the file with the given filename to:
|
||||
// 1) add remaining time lines M73 and update moves' gcode ids accordingly
|
||||
// 2) update used filament data
|
||||
void run_post_process();
|
||||
|
||||
// Additive second file-rewrite pass. Splices the pre-heat/pre-cool injector's InsertedLinesMap
|
||||
// into the finished g-code and re-shifts every move's gcode_id by the number of inserted lines
|
||||
// before it. Runs only when m_enable_pre_heating, AFTER run_post_process, so single-nozzle
|
||||
// printers (X1/P1/A1/H2S) never enter it; with an empty map it is a byte-for-byte identity rewrite.
|
||||
void run_second_pass_injection();
|
||||
// Shift each move's gcode_id by the count of injector lines inserted before it. No-op when the
|
||||
// map is empty.
|
||||
void handle_offsets_of_second_process(const TimeProcessor::InsertedLinesMap& inserted_operation_lines);
|
||||
|
||||
//BBS: different path_type is only used for arc move
|
||||
void store_move_vertex(EMoveType type, EMovePathType path_type = EMovePathType::Noop_move, bool internal_only = false);
|
||||
|
||||
void set_extrusion_role(ExtrusionRole role);
|
||||
// Resolve the SKIPPABLE_TYPE payload to a SkipType.
|
||||
void set_skippable_type(const std::string_view type);
|
||||
|
||||
float minimum_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
||||
float minimum_travel_feedrate(PrintEstimatedStatistics::ETimeMode mode, float feedrate) const;
|
||||
|
||||
@@ -361,7 +361,8 @@ namespace Slic3r {
|
||||
* @param safe_areas A collection of extended polygons defining the safe areas.
|
||||
* @return Point The nearest point within the safe areas or the default timelapse position if no safe areas exist.
|
||||
*/
|
||||
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision)
|
||||
Point pick_pos_internal(const Point& curr_pos, const ExPolygons& safe_areas, const ExPolygons& path_collision_area, bool detect_path_collision,
|
||||
const std::optional<Point>& farthest_point = std::nullopt)
|
||||
{
|
||||
struct CandidatePoint
|
||||
{
|
||||
@@ -381,7 +382,11 @@ namespace Slic3r {
|
||||
std::priority_queue<CandidatePoint> max_heap;
|
||||
|
||||
const double candidate_point_segment = scale_(5), weight_of_camera=1./3.;
|
||||
auto penaltyFunc = [&weight_of_camera](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
|
||||
auto penaltyFunc = [&weight_of_camera, &farthest_point](const Point &curr_post, const Point &CameraPos, const Point &candidatet) -> double {
|
||||
if (farthest_point.has_value()) {
|
||||
// Farthest-point timelapse: prefer candidate closest to the farthest point (L1 norm)
|
||||
return (farthest_point.value() - candidatet).cwiseAbs().sum();
|
||||
}
|
||||
// move distance + Camera occlusion penalty function
|
||||
double ret_pen = (curr_post - candidatet).cwiseAbs().sum() - weight_of_camera * (CameraPos - candidatet).cwiseAbs().sum();
|
||||
return ret_pen;
|
||||
@@ -523,7 +528,7 @@ namespace Slic3r {
|
||||
path_collision_area = union_ex(layer_slices_without_curr, rod_limit_areas);
|
||||
}
|
||||
|
||||
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object);
|
||||
return pick_pos_internal(center_p, safe_area,path_collision_area, by_object, ctx.farthest_point);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -610,4 +615,50 @@ namespace Slic3r {
|
||||
return *m_all_layer_pos;
|
||||
}
|
||||
|
||||
// Whether the head can travel to X0 without crossing any other instance that is
|
||||
// taller than the current print position.
|
||||
bool TimelapsePosPicker::get_is_clear_to_x0(const PosPickCtx &ctx)
|
||||
{
|
||||
bool by_object = m_print_seq == PrintSequence::ByObject;
|
||||
std::vector<const PrintObject *> object_list = get_object_list(ctx.printed_objects);
|
||||
|
||||
auto range_intersect = [](int left1, int right1, int left2, int right2) {
|
||||
if (left1 <= left2 && left2 <= right1) return true;
|
||||
if (left2 <= left1 && left1 <= right2) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
ExPolygons unclear_area;
|
||||
const Layer *layer = ctx.curr_layer;
|
||||
float z_target = layer->print_z;
|
||||
float z_low = layer->print_z - 0.5;
|
||||
float z_high = layer->print_z + 0.5;
|
||||
|
||||
for (auto &obj : object_list) {
|
||||
for (auto &instance : obj->instances()) {
|
||||
auto instance_bbox = get_real_instance_bbox(instance);
|
||||
bool is_curr_obj = ( obj == object_list.back() ) || ( !by_object ),
|
||||
higher_than_curr_pos = instance_bbox.max.z() > z_target;
|
||||
if (!is_curr_obj && range_intersect(instance_bbox.min.z(), instance_bbox.max.z(), z_low, z_high)) {
|
||||
ExPolygon expoly;
|
||||
expoly.contour = {{scale_(instance_bbox.min.x()), scale_(instance_bbox.min.y())},
|
||||
{scale_(instance_bbox.max.x()), scale_(instance_bbox.min.y())},
|
||||
{scale_(instance_bbox.max.x()), scale_(instance_bbox.max.y())},
|
||||
{scale_(instance_bbox.min.x()), scale_(instance_bbox.max.y())}};
|
||||
expoly.contour = expand_object_projection(expoly.contour, by_object, higher_than_curr_pos);
|
||||
unclear_area.emplace_back(std::move(expoly));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Point curr_pos_in_plate = {ctx.curr_pos.x() - scale_(m_plate_offset.x()), ctx.curr_pos.y() - scale_(m_plate_offset.y())};
|
||||
for (const ExPolygon &expoly : unclear_area) {
|
||||
BoundingBox bbox = expoly.contour.bounding_box();
|
||||
if (curr_pos_in_plate.y() < bbox.min.y() || curr_pos_in_plate.y() > bbox.max.y()) continue;
|
||||
if (bbox.min.x() <= curr_pos_in_plate.x()) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -22,6 +22,9 @@ namespace Slic3r {
|
||||
int picture_extruder_id; // the extruder id to take picture
|
||||
int curr_extruder_id;
|
||||
std::optional<std::vector<const PrintObject*>> printed_objects; // printed objects, only have value in by object mode
|
||||
// Farthest-point timelapse: plate-relative scaled point; when set, pick_pos_internal
|
||||
// biases the picked snapshot position toward this point (nullopt → legacy camera-occlusion loss).
|
||||
std::optional<Point> farthest_point;
|
||||
};
|
||||
|
||||
// data are stored without plate offset
|
||||
@@ -32,6 +35,9 @@ namespace Slic3r {
|
||||
~TimelapsePosPicker() = default;
|
||||
|
||||
Point pick_pos(const PosPickCtx& ctx);
|
||||
// Is the path to X0 clear of other (taller) instances? Drives the
|
||||
// `clear_to_x0` timelapse-gcode variable (g39 clamping detection).
|
||||
bool get_is_clear_to_x0(const PosPickCtx& ctx);
|
||||
void init(const Print* print, const Point& plate_offset);
|
||||
void reset();
|
||||
private:
|
||||
|
||||
@@ -7,13 +7,100 @@
|
||||
|
||||
namespace Slic3r
|
||||
{
|
||||
// ==================== MaxFlowWithLowerBounds ====================
|
||||
struct MaxFlowWithLowerBounds {
|
||||
public:
|
||||
|
||||
void add_edge(int from, int to, int capacity);
|
||||
|
||||
bool bfs();
|
||||
int dfs(int u, int f);
|
||||
int solve(std::vector<int>& matching);
|
||||
|
||||
public:
|
||||
std::vector<int> l_nodes;
|
||||
std::vector<int> r_nodes;
|
||||
std::vector<Edge> edges;
|
||||
std::vector<std::vector<int>> adj;
|
||||
std::vector<int> level;
|
||||
std::vector<int> it;
|
||||
|
||||
int total_nodes{ -1 };
|
||||
int source_id{ -1 };
|
||||
int sink_id{ -1 };
|
||||
};
|
||||
|
||||
void MaxFlowWithLowerBounds::add_edge(int from, int to, int capacity)
|
||||
{
|
||||
adj[from].emplace_back(edges.size());
|
||||
edges.emplace_back(from, to, capacity, 0);
|
||||
// also add the reverse residual edge with zero capacity
|
||||
adj[to].emplace_back(edges.size());
|
||||
edges.emplace_back(to, from, 0, 0);
|
||||
}
|
||||
|
||||
bool MaxFlowWithLowerBounds::bfs() {
|
||||
level.assign(total_nodes, -1);
|
||||
std::queue<int> q;
|
||||
q.push(source_id);
|
||||
level[source_id] = 0;
|
||||
|
||||
while (!q.empty()) {
|
||||
int u = q.front(); q.pop();
|
||||
for (int eid : adj[u]) {
|
||||
Edge &e = edges[eid];
|
||||
if (e.flow < e.capacity && level[e.to] == -1) {
|
||||
level[e.to] = level[u] + 1;
|
||||
q.push(e.to);
|
||||
}
|
||||
}
|
||||
}
|
||||
return level[sink_id] != -1;
|
||||
}
|
||||
|
||||
int MaxFlowWithLowerBounds::dfs(int u, int f) {
|
||||
if (u == sink_id) return f;
|
||||
for (int &i = it[u]; i < (int)adj[u].size(); ++i) {
|
||||
int eid = adj[u][i];
|
||||
Edge &e = edges[eid];
|
||||
if (e.flow < e.capacity && level[e.to] == level[u] + 1) {
|
||||
int pushed = dfs(e.to, std::min(f, e.capacity - e.flow));
|
||||
if (pushed > 0) {
|
||||
e.flow += pushed;
|
||||
edges[eid ^ 1].flow -= pushed;
|
||||
return pushed;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int MaxFlowWithLowerBounds::solve(std::vector<int>& matching) {
|
||||
int flow = 0;
|
||||
while (bfs()) {
|
||||
it.assign(total_nodes, 0);
|
||||
while (int pushed = dfs(source_id, MaxFlowGraph::INF))
|
||||
flow += pushed;
|
||||
}
|
||||
|
||||
int L = l_nodes.size();
|
||||
int R = r_nodes.size();
|
||||
// collect l-r matches
|
||||
matching.resize(l_nodes.size(), MaxFlowGraph::INVALID_ID);
|
||||
for (int u = 0; u < L; ++u) {
|
||||
for (int eid : adj[u]) {
|
||||
Edge &e = edges[eid];
|
||||
if (e.flow > 0 && e.to >= L && e.to < L + R) {
|
||||
matching[e.from] = e.to - L;
|
||||
}
|
||||
}
|
||||
}
|
||||
return flow;
|
||||
}
|
||||
|
||||
// ==================== MinCostMaxFlow ====================
|
||||
struct MinCostMaxFlow {
|
||||
public:
|
||||
struct Edge {
|
||||
int from, to, capacity, cost, flow;
|
||||
Edge(int u, int v, int cap, int cst) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
|
||||
};
|
||||
|
||||
std::vector<int> solve();
|
||||
void add_edge(int from, int to, int capacity, int cost);
|
||||
bool spfa(int source, int sink);
|
||||
@@ -107,15 +194,10 @@ namespace Slic3r
|
||||
{
|
||||
if (l_nodes[idx_in_left] == -1) {
|
||||
return 0;
|
||||
//TODO: test more here
|
||||
int sum = 0;
|
||||
for (int i = 0; i < matrix.size(); ++i)
|
||||
sum += matrix[i][idx_in_right];
|
||||
sum /= matrix.size();
|
||||
return -sum;
|
||||
}
|
||||
|
||||
return matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
||||
float val = matrix[l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
||||
return std::min(static_cast<int>(val), MaxFlowGraph::MCMF_MAX_EDGE_COST);
|
||||
}
|
||||
|
||||
|
||||
@@ -123,27 +205,40 @@ namespace Slic3r
|
||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
||||
const std::vector<int>& u_capacity,
|
||||
const std::vector<int>& v_capacity)
|
||||
const std::vector<int>& v_capacity,
|
||||
const std::vector<std::pair<std::set<int>,int>>& v_group_capacity)
|
||||
{
|
||||
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
||||
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
||||
l_nodes = u_nodes;
|
||||
r_nodes = v_nodes;
|
||||
total_nodes = u_nodes.size() + v_nodes.size() + 2;
|
||||
total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
|
||||
source_id = total_nodes - 2;
|
||||
sink_id = total_nodes - 1;
|
||||
|
||||
adj.resize(total_nodes);
|
||||
|
||||
std::vector<int>v_node_to(v_nodes.size(), sink_id);
|
||||
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
|
||||
for (auto vid : v_group_capacity[gid].first)
|
||||
v_node_to[vid] = l_nodes.size() + r_nodes.size() + gid;
|
||||
}
|
||||
|
||||
// add edge from source to left nodes
|
||||
for (int idx = 0; idx < l_nodes.size(); ++idx) {
|
||||
int capacity = u_capacity.empty() ? 1 : u_capacity[idx];
|
||||
add_edge(source_id, idx, capacity);
|
||||
}
|
||||
// add edge from right nodes to sink node
|
||||
// add edge from right nodes to v_node_to(sink node or temp group node)
|
||||
for (int idx = 0; idx < r_nodes.size(); ++idx) {
|
||||
int capacity = v_capacity.empty() ? 1 : v_capacity[idx];
|
||||
add_edge(l_nodes.size() + idx, sink_id, capacity);
|
||||
add_edge(l_nodes.size() + idx, v_node_to[idx], capacity);
|
||||
}
|
||||
|
||||
// add edge from temp group node to sink node
|
||||
for (int idx = 0; idx < v_group_capacity.size(); ++idx) {
|
||||
int capacity = v_group_capacity[idx].second;
|
||||
add_edge(l_nodes.size() + r_nodes.size() + idx, sink_id, capacity);
|
||||
}
|
||||
|
||||
// add edge from left nodes to right nodes
|
||||
@@ -269,6 +364,301 @@ namespace Slic3r
|
||||
return m_solver->solve();
|
||||
}
|
||||
|
||||
// ==================== GeneralMinCostLowerBoundsSolver ====================
|
||||
GeneralMinCostLowerBoundsSolver::~GeneralMinCostLowerBoundsSolver() = default;
|
||||
|
||||
GeneralMinCostLowerBoundsSolver::GeneralMinCostLowerBoundsSolver(const std::vector<FlushMatrix> &matrix_,
|
||||
const std::vector<int> &u_nodes,
|
||||
const std::vector<int> &v_nodes,
|
||||
const std::vector<int> &v_nodes_group,
|
||||
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
|
||||
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
|
||||
{
|
||||
flush_matrix = matrix_;
|
||||
l_nodes = u_nodes;
|
||||
r_nodes = v_nodes;
|
||||
r_nodes_group = v_nodes_group;
|
||||
m_uv_link_limits = uv_link_limits;
|
||||
m_uv_unlink_limits = uv_unlink_limits;
|
||||
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
|
||||
|
||||
m_solver_lower_bounds = std::make_unique<MaxFlowWithLowerBounds>();
|
||||
m_solver_min_cost = std::make_unique<MinCostMaxFlow>();
|
||||
}
|
||||
|
||||
std::vector<int> GeneralMinCostLowerBoundsSolver::solve()
|
||||
{
|
||||
// group nodes that do not need a lower-bound constraint
|
||||
std::unordered_set<int> no_lower_group;
|
||||
for (int i = 0; i < r_nodes.size(); i++) {
|
||||
if (r_nodes[i] >= 0)
|
||||
no_lower_group.insert(r_nodes_group[i]);
|
||||
}
|
||||
|
||||
// 1. build the lower-bound network graph
|
||||
build_feasible_graph(no_lower_group);
|
||||
|
||||
// 2. compute the max flow
|
||||
int need = 0;
|
||||
for (int d : demand)
|
||||
if (d > 0) need += d;
|
||||
std::vector<int> feasible_matching;
|
||||
int pushed_flow = m_solver_lower_bounds->solve(feasible_matching);
|
||||
assert(need == pushed_flow);
|
||||
|
||||
// 3. convert the lower-bound max-flow network into a min-cost-max-flow network
|
||||
build_graph_with_feasible_result();
|
||||
// 4. compute the min-cost max-flow
|
||||
auto min_cost_matching = m_solver_min_cost->solve();
|
||||
|
||||
return min_cost_matching;
|
||||
}
|
||||
|
||||
void GeneralMinCostLowerBoundsSolver::build_feasible_graph(const std::unordered_set<int> &no_lower_groups)
|
||||
{
|
||||
m_solver_lower_bounds->l_nodes = l_nodes;
|
||||
m_solver_lower_bounds->r_nodes = r_nodes;
|
||||
m_solver_lower_bounds->total_nodes = l_nodes.size() + r_nodes.size() + num_groups + 2;
|
||||
|
||||
m_solver_lower_bounds->source_id = m_solver_lower_bounds->total_nodes - 2;
|
||||
m_solver_lower_bounds->sink_id = m_solver_lower_bounds->total_nodes - 1;
|
||||
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
|
||||
demand.resize(m_solver_lower_bounds->total_nodes, 0);
|
||||
|
||||
const int L = m_solver_lower_bounds->l_nodes.size();
|
||||
const int R = m_solver_lower_bounds->r_nodes.size();
|
||||
|
||||
// source -> l
|
||||
for (int i = 0; i < L; ++i)
|
||||
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->source_id, i, 1);
|
||||
|
||||
// u -> v (with link/unlink limits)
|
||||
for (int i = 0; i < L; ++i) {
|
||||
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||
for (int j : it->second)
|
||||
m_solver_lower_bounds->add_edge(i, L + j, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<std::vector<int>> unlink_limits;
|
||||
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||
unlink_limits = it->second;
|
||||
|
||||
for (int j = 0; j < R; ++j) {
|
||||
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||
continue;
|
||||
m_solver_lower_bounds->add_edge(i, L + j, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// r -> group
|
||||
for (int j = 0; j < R; ++j) {
|
||||
int g = r_nodes_group[j];
|
||||
m_solver_lower_bounds->add_edge(L + j, L + R + g, 1);
|
||||
}
|
||||
|
||||
// group -> sink (lower bound = 1)
|
||||
for (int g = 0; g < num_groups; ++g) {
|
||||
if (no_lower_groups.count(g))
|
||||
m_solver_lower_bounds->add_edge(L + R + g, m_solver_lower_bounds->sink_id, R);
|
||||
else
|
||||
add_edge_with_lower_bound(L + R + g, m_solver_lower_bounds->sink_id, 1, R, 0);
|
||||
}
|
||||
|
||||
max_flow_edges = m_solver_lower_bounds->edges.size();
|
||||
|
||||
// support lower bounds, add super source super sink
|
||||
super_source = m_solver_lower_bounds->total_nodes++;
|
||||
super_sink = m_solver_lower_bounds->total_nodes++;
|
||||
|
||||
m_solver_lower_bounds->adj.resize(m_solver_lower_bounds->total_nodes);
|
||||
demand.resize(m_solver_lower_bounds->total_nodes, 0);
|
||||
|
||||
for (int i = 0; i < super_source; ++i) {
|
||||
if (demand[i] > 0) {
|
||||
m_solver_lower_bounds->add_edge(super_source, i, demand[i]);
|
||||
} else if (demand[i] < 0) {
|
||||
m_solver_lower_bounds->add_edge(i, super_sink, -demand[i]);
|
||||
}
|
||||
}
|
||||
m_solver_lower_bounds->add_edge(m_solver_lower_bounds->sink_id, m_solver_lower_bounds->source_id, MaxFlowGraph::INF);
|
||||
source_id = m_solver_lower_bounds->source_id;
|
||||
sink_id = m_solver_lower_bounds->sink_id;
|
||||
m_solver_lower_bounds->source_id = super_source;
|
||||
m_solver_lower_bounds->sink_id = super_sink;
|
||||
}
|
||||
|
||||
void GeneralMinCostLowerBoundsSolver::build_graph_with_feasible_result()
|
||||
{
|
||||
for (auto&lb:lower_bound_edges){
|
||||
m_solver_lower_bounds->edges[lb.edge_id].flow += lb.lower;
|
||||
m_solver_lower_bounds->edges[lb.edge_id ^ 1].flow -= lb.lower;
|
||||
}
|
||||
|
||||
m_solver_min_cost->l_nodes = m_solver_lower_bounds->l_nodes;
|
||||
m_solver_min_cost->r_nodes = m_solver_lower_bounds->r_nodes;
|
||||
|
||||
m_solver_min_cost->source_id = source_id;
|
||||
m_solver_min_cost->sink_id = sink_id;
|
||||
m_solver_min_cost->total_nodes = sink_id + 1;
|
||||
|
||||
m_solver_min_cost->edges = m_solver_lower_bounds->edges;
|
||||
m_solver_min_cost->edges.erase(m_solver_min_cost->edges.begin() + max_flow_edges, m_solver_min_cost->edges.end());
|
||||
|
||||
m_solver_min_cost->adj = m_solver_lower_bounds->adj;
|
||||
m_solver_min_cost->adj.resize(m_solver_min_cost->total_nodes);
|
||||
for (auto &node_edges : m_solver_min_cost->adj) {
|
||||
node_edges.erase(std::remove_if(node_edges.begin(), node_edges.end(), [this](int val) {return val >= this->max_flow_edges;}), node_edges.end());
|
||||
}
|
||||
|
||||
|
||||
for (auto& e : m_solver_min_cost->edges) {
|
||||
int L = m_solver_min_cost->l_nodes.size();
|
||||
int R = m_solver_min_cost->r_nodes.size();
|
||||
|
||||
if (e.from < L && e.to >= L && e.to < L + R) {
|
||||
int idx_in_left = e.from;
|
||||
int idx_in_right = e.to - L;
|
||||
int group_id = r_nodes_group[idx_in_right];
|
||||
|
||||
if (r_nodes[idx_in_right] == -1) continue;
|
||||
e.cost = flush_matrix[group_id][l_nodes[idx_in_left]][r_nodes[idx_in_right]];
|
||||
}
|
||||
}
|
||||
}
|
||||
void GeneralMinCostLowerBoundsSolver::add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost)
|
||||
{
|
||||
int eid = m_solver_lower_bounds->edges.size();
|
||||
m_solver_lower_bounds->add_edge(from, to, upper - lower);
|
||||
|
||||
lower_bound_edges.push_back({eid, lower});
|
||||
demand[from] -= lower;
|
||||
demand[to] += lower;
|
||||
}
|
||||
|
||||
// ==================== GroupMinCostFlowSolver ====================
|
||||
GroupMinCostFlowSolver::~GroupMinCostFlowSolver() = default;
|
||||
|
||||
GroupMinCostFlowSolver::GroupMinCostFlowSolver(const std::vector<FlushMatrix> &matrix_,
|
||||
const std::vector<int> &u_nodes,
|
||||
const std::vector<int> &v_nodes,
|
||||
const std::vector<int> &v_nodes_group,
|
||||
const std::unordered_map<int, std::vector<int>> &uv_link_limits,
|
||||
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits)
|
||||
{
|
||||
flush_matrix = matrix_;
|
||||
l_nodes = u_nodes;
|
||||
r_nodes = v_nodes;
|
||||
r_nodes_group = v_nodes_group;
|
||||
m_uv_link_limits = uv_link_limits;
|
||||
m_uv_unlink_limits = uv_unlink_limits;
|
||||
num_groups = *std::max_element(r_nodes_group.begin(), r_nodes_group.end()) + 1;
|
||||
|
||||
m_solver = std::make_unique<MinCostMaxFlow>();
|
||||
build_graph();
|
||||
}
|
||||
|
||||
int GroupMinCostFlowSolver::get_flush_cost(int l_idx, int r_idx)
|
||||
{
|
||||
if (r_nodes[r_idx] == -1)
|
||||
return 0;
|
||||
int group_id = r_nodes_group[r_idx];
|
||||
return (int)flush_matrix[group_id][l_nodes[l_idx]][r_nodes[r_idx]];
|
||||
}
|
||||
|
||||
void GroupMinCostFlowSolver::build_graph()
|
||||
{
|
||||
const int L = (int)l_nodes.size();
|
||||
const int R = (int)r_nodes.size();
|
||||
const int G = num_groups;
|
||||
|
||||
m_solver->l_nodes = l_nodes;
|
||||
m_solver->r_nodes = r_nodes;
|
||||
m_solver->total_nodes = L + R + G + 2;
|
||||
m_solver->source_id = L + R + G;
|
||||
m_solver->sink_id = L + R + G + 1;
|
||||
m_solver->adj.resize(m_solver->total_nodes);
|
||||
|
||||
int max_flush = 0;
|
||||
for (const auto &mat : flush_matrix)
|
||||
for (const auto &row : mat)
|
||||
for (float v : row)
|
||||
max_flush = std::max(max_flush, (int)v);
|
||||
int bonus = max_flush * L + 1;
|
||||
|
||||
// source -> l_i
|
||||
for (int i = 0; i < L; ++i)
|
||||
m_solver->add_edge(m_solver->source_id, i, 1, 0);
|
||||
|
||||
// l_i -> r_j (with link/unlink limits)
|
||||
for (int i = 0; i < L; ++i) {
|
||||
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||
for (int j : it->second)
|
||||
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
|
||||
continue;
|
||||
}
|
||||
|
||||
std::optional<std::vector<int>> unlink_limits;
|
||||
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||
unlink_limits = it->second;
|
||||
|
||||
for (int j = 0; j < R; ++j) {
|
||||
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||
continue;
|
||||
m_solver->add_edge(i, L + j, 1, get_flush_cost(i, j));
|
||||
}
|
||||
}
|
||||
|
||||
// r_j -> group_g
|
||||
// Compute per-nozzle incoming edge count as capacity upper bound.
|
||||
// When unlink_limits restrict multiple filaments to the same nozzle,
|
||||
// capacity=1 would block valid assignments. Using the actual in-degree
|
||||
// allows the necessary flow while still preserving nozzle-level balance
|
||||
// (a nozzle with fewer forced filaments keeps a tighter cap).
|
||||
// The first unit carries a small nozzle-bonus to encourage spreading
|
||||
// filaments across distinct nozzles within the same group.
|
||||
int nozzle_bonus = max_flush + 1;
|
||||
std::vector<int> r_in_degree(R, 0);
|
||||
for (int i = 0; i < L; ++i) {
|
||||
if (auto it = m_uv_link_limits.find(i); it != m_uv_link_limits.end()) {
|
||||
for (int j : it->second)
|
||||
r_in_degree[j]++;
|
||||
continue;
|
||||
}
|
||||
std::optional<std::vector<int>> unlink_limits;
|
||||
if (auto it = m_uv_unlink_limits.find(i); it != m_uv_unlink_limits.end())
|
||||
unlink_limits = it->second;
|
||||
for (int j = 0; j < R; ++j) {
|
||||
if (unlink_limits.has_value() && std::find(unlink_limits->begin(), unlink_limits->end(), j) != unlink_limits->end())
|
||||
continue;
|
||||
r_in_degree[j]++;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < R; ++j) {
|
||||
int g = r_nodes_group[j];
|
||||
int cap = std::max(r_in_degree[j], 1);
|
||||
// First unit gets -nozzle_bonus to prefer using distinct nozzles
|
||||
m_solver->add_edge(L + j, L + R + g, 1, -nozzle_bonus);
|
||||
if (cap > 1)
|
||||
m_solver->add_edge(L + j, L + R + g, cap - 1, 0);
|
||||
}
|
||||
|
||||
// group_g -> sink (split: first unit gets -bonus, rest gets 0)
|
||||
// bonus >> nozzle_bonus, so group coverage always takes priority
|
||||
for (int g = 0; g < G; ++g) {
|
||||
m_solver->add_edge(L + R + g, m_solver->sink_id, 1, -bonus);
|
||||
if (L > 1)
|
||||
m_solver->add_edge(L + R + g, m_solver->sink_id, L - 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<int> GroupMinCostFlowSolver::solve()
|
||||
{
|
||||
return m_solver->solve();
|
||||
}
|
||||
|
||||
// ==================== MinFlushFlowSolver ====================
|
||||
MinFlushFlowSolver::~MinFlushFlowSolver()
|
||||
{
|
||||
}
|
||||
@@ -277,7 +667,8 @@ namespace Slic3r
|
||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits,
|
||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits,
|
||||
const std::vector<int>& u_capacity,
|
||||
const std::vector<int>& v_capacity)
|
||||
const std::vector<int>& v_capacity,
|
||||
const std::vector<std::pair<std::set<int>,int>>&v_group_capacity)
|
||||
{
|
||||
assert(u_capacity.empty() || u_capacity.size() == u_nodes.size());
|
||||
assert(v_capacity.empty() || v_capacity.size() == v_nodes.size());
|
||||
@@ -286,13 +677,19 @@ namespace Slic3r
|
||||
m_solver->l_nodes = u_nodes;
|
||||
m_solver->r_nodes = v_nodes;
|
||||
|
||||
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + 2;
|
||||
m_solver->total_nodes = u_nodes.size() + v_nodes.size() + v_group_capacity.size() + 2;
|
||||
|
||||
m_solver->source_id =m_solver->total_nodes - 2;
|
||||
m_solver->sink_id = m_solver->total_nodes - 1;
|
||||
|
||||
m_solver->adj.resize(m_solver->total_nodes);
|
||||
|
||||
std::vector<int> v_node_to(v_nodes.size(), m_solver->sink_id);
|
||||
for (size_t gid = 0; gid < v_group_capacity.size(); ++gid) {
|
||||
for (auto vid : v_group_capacity[gid].first)
|
||||
v_node_to[vid] = m_solver->l_nodes.size() + m_solver->r_nodes.size() + gid;
|
||||
}
|
||||
|
||||
// add edge from source to left nodes,cost to 0
|
||||
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
||||
int capacity = u_capacity.empty() ? 1 : u_capacity[i];
|
||||
@@ -301,7 +698,12 @@ namespace Slic3r
|
||||
// add edge from right nodes to sink,cost to 0
|
||||
for (int i = 0; i < m_solver->r_nodes.size(); ++i) {
|
||||
int capacity = v_capacity.empty() ? 1 : v_capacity[i];
|
||||
m_solver->add_edge(m_solver->l_nodes.size() + i, m_solver->sink_id, capacity, 0);
|
||||
m_solver->add_edge(m_solver->l_nodes.size() + i, v_node_to[i], capacity, 0);
|
||||
}
|
||||
// add edge from temp group node to sink node
|
||||
for(int i=0;i<v_group_capacity.size();++i){
|
||||
int capacity = v_group_capacity[i].second;
|
||||
m_solver->add_edge(m_solver->l_nodes.size() + m_solver->r_nodes.size() + i, m_solver->sink_id, capacity, 0);
|
||||
}
|
||||
// add edge from left node to right nodes
|
||||
for (int i = 0; i < m_solver->l_nodes.size(); ++i) {
|
||||
@@ -602,12 +1004,125 @@ namespace Slic3r
|
||||
}
|
||||
|
||||
|
||||
// Single-nozzle flush-minimizing reorder over one filament set / one flush matrix, with an
|
||||
// optional seed filament. Extracted from the group loop so the multi-nozzle reorder can call it
|
||||
// per physical nozzle.
|
||||
// TODO: add custom sequence
|
||||
static int reorder_filaments_for_minimum_flush_volume_base(const std::vector<unsigned int>& filament_lists,
|
||||
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||
const FlushMatrix& flush_matrix,
|
||||
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
|
||||
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||
std::optional<unsigned int> initial_filament_id = std::nullopt)
|
||||
{
|
||||
constexpr int max_n_with_forcast = 5;
|
||||
using uint128_t = boost::multiprecision::uint128_t;
|
||||
|
||||
if (filament_sequences) {
|
||||
filament_sequences->clear();
|
||||
filament_sequences->reserve(layer_filaments.size());
|
||||
}
|
||||
auto filament_list_to_hash_key = [](const std::vector<unsigned int>& curr_layer_filaments, const std::vector<unsigned int>& next_layer_filaments,
|
||||
const std::optional<unsigned int>& prev_filament, bool use_forcast) -> uint128_t {
|
||||
uint128_t hash_key = 0;
|
||||
// 31-0 bit define current layer extruder,63-32 bit define next layer extruder,95~64 define prev extruder
|
||||
if (prev_filament) hash_key |= (uint128_t(1) << (64 + *prev_filament));
|
||||
|
||||
if (use_forcast) {
|
||||
for (auto item : next_layer_filaments) { hash_key |= (uint128_t(1) << (32 + item)); }
|
||||
}
|
||||
|
||||
for (auto item : curr_layer_filaments) { hash_key |= (uint128_t(1) << item); }
|
||||
return hash_key;
|
||||
};
|
||||
|
||||
int cost = 0;
|
||||
std::map<size_t, std::vector<unsigned int>> custom_layer_sequence_map;
|
||||
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
|
||||
std::unordered_set<unsigned int> filament_sets(filament_lists.begin(), filament_lists.end());
|
||||
std::optional<unsigned int> curr_filament_id;
|
||||
// use the provided initial filament id as the starting state when it is valid
|
||||
if (initial_filament_id.has_value() && *initial_filament_id < flush_matrix.size()) {
|
||||
curr_filament_id = initial_filament_id;
|
||||
}
|
||||
|
||||
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
|
||||
const auto& curr_lf = layer_filaments[layer];
|
||||
std::vector<int> custom_filament_seq;
|
||||
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
|
||||
std::vector<unsigned int> unsign_custom_extruder_seq;
|
||||
for (int extruder : custom_filament_seq) {
|
||||
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
|
||||
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
|
||||
if (it != layer_filaments[layer].end())
|
||||
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
|
||||
}
|
||||
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
|
||||
|
||||
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
|
||||
const auto& curr_lf = layer_filaments[layer];
|
||||
|
||||
if(auto iter = custom_layer_sequence_map.find(layer); iter != custom_layer_sequence_map.end()){
|
||||
auto sequence_in_group = collect_filaments_in_groups<unsigned int>(std::unordered_set<unsigned int>(filament_lists.begin(),filament_lists.end()), iter->second);
|
||||
|
||||
std::optional<unsigned int> prev = curr_filament_id;
|
||||
for (auto& f: sequence_in_group){
|
||||
if(prev)
|
||||
cost += flush_matrix[*prev][f];
|
||||
prev = f;
|
||||
}
|
||||
|
||||
if(!sequence_in_group.empty()){
|
||||
curr_filament_id = sequence_in_group.back();
|
||||
}
|
||||
|
||||
if(filament_sequences)
|
||||
filament_sequences->emplace_back(sequence_in_group);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<unsigned int> filament_used = collect_filaments_in_groups<unsigned int>(filament_sets, curr_lf);
|
||||
std::vector<unsigned int> next_lf;
|
||||
if (layer + 1 < layer_filaments.size()) next_lf = layer_filaments[layer + 1];
|
||||
std::vector<unsigned int> filament_used_next_layer = collect_filaments_in_groups<unsigned int>(filament_sets, next_lf);
|
||||
|
||||
bool use_forcast = false;
|
||||
float tmp_cost = 0;
|
||||
std::vector<unsigned int> sequence;
|
||||
uint128_t hash_key = filament_list_to_hash_key(filament_used, filament_used_next_layer, curr_filament_id, use_forcast);
|
||||
if (auto iter = caches.find(hash_key); iter != caches.end()) {
|
||||
tmp_cost = iter->second.first;
|
||||
sequence = iter->second.second;
|
||||
}
|
||||
else {
|
||||
sequence = get_extruders_order(flush_matrix, filament_used, filament_used_next_layer, curr_filament_id, use_forcast, &tmp_cost);
|
||||
caches[hash_key] = { tmp_cost,sequence };
|
||||
}
|
||||
|
||||
if (filament_sequences)
|
||||
filament_sequences->emplace_back(sequence);
|
||||
|
||||
if (!sequence.empty())
|
||||
curr_filament_id = sequence.back();
|
||||
|
||||
cost += tmp_cost;
|
||||
}
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int>& filament_lists,
|
||||
const std::vector<int>& filament_maps,
|
||||
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||
const std::vector<FlushMatrix>& flush_matrix,
|
||||
std::optional<std::function<bool(int, std::vector<int>&)>> get_custom_seq,
|
||||
std::vector<std::vector<unsigned int>>* filament_sequences)
|
||||
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||
const std::unordered_map<int, int>& nozzle_status)
|
||||
{
|
||||
//only when layer filament num <= 5,we do forcast
|
||||
constexpr int max_n_with_forcast = 5;
|
||||
@@ -670,6 +1185,12 @@ namespace Slic3r
|
||||
if (groups[idx].empty())
|
||||
continue;
|
||||
std::optional<unsigned int>current_extruder_id;
|
||||
// seed the group (nozzle) with the filament already loaded, if nozzle_status supplies one
|
||||
if (auto it = nozzle_status.find(static_cast<int>(idx)); it != nozzle_status.end() && it->second >= 0) {
|
||||
unsigned int initial_fil = static_cast<unsigned int>(it->second);
|
||||
if (initial_fil < flush_matrix[idx].size())
|
||||
current_extruder_id = initial_fil;
|
||||
}
|
||||
|
||||
std::unordered_map<uint128_t, std::pair<float, std::vector<unsigned int>>> caches;
|
||||
|
||||
@@ -775,4 +1296,174 @@ namespace Slic3r
|
||||
|
||||
return cost;
|
||||
}
|
||||
|
||||
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
|
||||
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||
const std::vector<FlushMatrix>& flush_matrix,
|
||||
const std::function<bool(int, std::vector<int>&)> get_custom_seq,
|
||||
std::vector<std::vector<unsigned int>>* filament_sequences,
|
||||
const MultiNozzleUtils::NozzleStatusRecorder& initial_status)
|
||||
{
|
||||
std::map<int,std::set<unsigned int>> nozzle_filament_groups;
|
||||
std::map<int,std::set<int>> extruder_to_nozzle;
|
||||
|
||||
for(auto filament_idx : filament_lists){
|
||||
auto nozzle_info = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||
if (!nozzle_info)
|
||||
continue;
|
||||
nozzle_filament_groups[nozzle_info->group_id].insert(filament_idx);
|
||||
extruder_to_nozzle[nozzle_info->extruder_id].insert(nozzle_info->group_id);
|
||||
}
|
||||
|
||||
std::map<size_t, std::vector<unsigned int>>custom_layer_sequence_map;// save the filament sequences of custom layer
|
||||
for (size_t layer = 0; layer < layer_filaments.size(); ++layer){
|
||||
const auto& curr_lf = layer_filaments[layer];
|
||||
std::vector<int> custom_filament_seq;
|
||||
if (get_custom_seq && get_custom_seq(layer, custom_filament_seq) && !custom_filament_seq.empty()) {
|
||||
std::vector<unsigned int> unsign_custom_extruder_seq;
|
||||
for (int extruder : custom_filament_seq) {
|
||||
unsigned int unsign_extruder = static_cast<unsigned int>(extruder) - 1;
|
||||
auto it = std::find(layer_filaments[layer].begin(), layer_filaments[layer].end(), unsign_extruder);
|
||||
if (it != layer_filaments[layer].end())
|
||||
unsign_custom_extruder_seq.emplace_back(unsign_extruder);
|
||||
}
|
||||
assert(layer_filaments[layer].size() == unsign_custom_extruder_seq.size());
|
||||
|
||||
custom_layer_sequence_map[layer] = unsign_custom_extruder_seq;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::map<int, std::vector<std::vector<unsigned int>>> nozzle_filament_sequences;
|
||||
bool store_sequence = filament_sequences != nullptr;
|
||||
|
||||
int cost = 0;
|
||||
for(auto& group : nozzle_filament_groups){
|
||||
int nozzle_id = group.first;
|
||||
auto& filament_in_nozzle = group.second;
|
||||
|
||||
int extruder_id = 0;
|
||||
for(auto& [ext, nozzle_set] : extruder_to_nozzle){
|
||||
if(nozzle_set.count(nozzle_id)){
|
||||
extruder_id = ext;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(filament_in_nozzle.empty())
|
||||
continue;
|
||||
|
||||
std::vector<unsigned int> filament_vec_in_nozzle(filament_in_nozzle.begin(), filament_in_nozzle.end());
|
||||
|
||||
int initial_fil = initial_status.get_filament_in_nozzle(nozzle_id);
|
||||
std::optional<unsigned int> initial_fil_id = (initial_fil >= 0 && initial_fil < flush_matrix[extruder_id].size())? std::optional<unsigned int>(initial_fil) : std::nullopt;
|
||||
|
||||
std::vector<std::vector<unsigned int>> filament_seq;
|
||||
cost += reorder_filaments_for_minimum_flush_volume_base(filament_vec_in_nozzle, layer_filaments, flush_matrix[extruder_id], get_custom_seq,
|
||||
store_sequence ? &filament_seq : nullptr, initial_fil_id);
|
||||
if(store_sequence)
|
||||
nozzle_filament_sequences.emplace(nozzle_id, std::move(filament_seq));
|
||||
|
||||
}
|
||||
|
||||
if(!store_sequence)
|
||||
return cost;
|
||||
|
||||
std::vector<int> extruders;
|
||||
std::map<int, std::vector<int>> nozzles_per_extruder;
|
||||
for (auto& [extruder_id, nozzle_set] : extruder_to_nozzle) {
|
||||
extruders.push_back(extruder_id);
|
||||
nozzles_per_extruder[extruder_id] = std::vector<int>(
|
||||
nozzle_set.begin(), nozzle_set.end()
|
||||
);
|
||||
}
|
||||
|
||||
filament_sequences->clear();
|
||||
filament_sequences->resize(layer_filaments.size());
|
||||
|
||||
// No filament in filament_lists resolved to a nozzle in nozzle_group_result
|
||||
// (e.g. a degenerate input where a layer references a filament index outside the range's
|
||||
// grouping map). Emit each layer's filaments in their given order so the caller still gets a
|
||||
// valid per-layer sequence, and skip the cross-nozzle reorder. Guards the unchecked
|
||||
// max_element(extruders) below, which would dereference end() on an empty range.
|
||||
if (extruders.empty()) {
|
||||
for (size_t layer = 0; layer < layer_filaments.size(); ++layer)
|
||||
(*filament_sequences)[layer] = layer_filaments[layer];
|
||||
return cost;
|
||||
}
|
||||
|
||||
auto get_extruder_for_filament = [nozzle_group_result](unsigned int filament_idx) {
|
||||
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||
if (!nozzle)
|
||||
return -1;
|
||||
return nozzle->extruder_id;
|
||||
};
|
||||
|
||||
auto get_nozzle_idx_for_filament = [nozzles_per_extruder, nozzle_group_result](unsigned int filament_idx)->int {
|
||||
auto nozzle = nozzle_group_result.get_nozzle_for_filament(filament_idx, -1);
|
||||
if (!nozzle)
|
||||
return -1;
|
||||
return std::find(nozzles_per_extruder.at(nozzle->extruder_id).begin(), nozzles_per_extruder.at(nozzle->extruder_id).end(), nozzle->group_id) - nozzles_per_extruder.at(nozzle->extruder_id).begin();
|
||||
};
|
||||
|
||||
int initial_extruder = initial_status.get_current_extruder_id();
|
||||
int last_extruder_idx = (initial_extruder >= 0 && initial_extruder < extruders.size())? initial_extruder : 0;
|
||||
// set size to max extruder_id in case extruder_id is not continuous
|
||||
std::vector<int> last_nozzle_idx(*std::max_element(extruders.begin(),extruders.end()) + 1,0);
|
||||
for (int ext_id = 0; ext_id < static_cast<int>(last_nozzle_idx.size()); ext_id++) {
|
||||
int initial_nozzle = initial_status.get_nozzle_in_extruder(ext_id);
|
||||
auto ext_nozzles = nozzles_per_extruder[ext_id];
|
||||
auto it = std::find(ext_nozzles.begin(), ext_nozzles.end(), initial_nozzle);
|
||||
if (it != ext_nozzles.end())
|
||||
last_nozzle_idx[ext_id] = static_cast<int>(std::distance(ext_nozzles.begin(), it));
|
||||
}
|
||||
|
||||
for (size_t layer = 0; layer < layer_filaments.size(); ++layer) {
|
||||
auto& out_seq = (*filament_sequences)[layer];
|
||||
|
||||
if (custom_layer_sequence_map.find(layer) != custom_layer_sequence_map.end()) {
|
||||
out_seq = custom_layer_sequence_map[layer];
|
||||
if (!out_seq.empty()) {
|
||||
last_extruder_idx = get_extruder_for_filament(out_seq.back());
|
||||
for (auto filament : out_seq) {
|
||||
int cur_ext_id = get_extruder_for_filament(filament);
|
||||
last_nozzle_idx[cur_ext_id] = get_nozzle_idx_for_filament(filament);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (last_extruder_idx == -1)
|
||||
last_extruder_idx = 0;
|
||||
|
||||
int curr_last_extruder_idx = last_extruder_idx;
|
||||
auto curr_last_nozzle_idx = last_nozzle_idx;
|
||||
for (int i = 0; i < extruders.size(); ++i) {
|
||||
int extruder_id = extruders[(last_extruder_idx + i) % extruders.size()];
|
||||
auto& base_nozzles = nozzles_per_extruder[extruder_id];
|
||||
|
||||
bool has_seq = false;
|
||||
if (last_nozzle_idx[extruder_id] == -1)
|
||||
last_nozzle_idx[extruder_id] = 0;
|
||||
|
||||
for (int j = 0; j < base_nozzles.size(); ++j) {
|
||||
int nozzle_idx = (last_nozzle_idx[extruder_id] + j) % base_nozzles.size();
|
||||
int nozzle_id = base_nozzles[nozzle_idx];
|
||||
const auto& frag = nozzle_filament_sequences[nozzle_id][layer];
|
||||
if (frag.empty())
|
||||
continue;
|
||||
has_seq = true;
|
||||
curr_last_nozzle_idx[extruder_id] = nozzle_idx;
|
||||
out_seq.insert(out_seq.end(), frag.begin(), frag.end());
|
||||
}
|
||||
|
||||
if (has_seq)
|
||||
curr_last_extruder_idx = extruder_id;
|
||||
}
|
||||
last_extruder_idx = curr_last_extruder_idx;
|
||||
last_nozzle_idx = curr_last_nozzle_idx;
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include "../MultiNozzleUtils.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -15,21 +18,27 @@ using FlushMatrix = std::vector<std::vector<float>>;
|
||||
namespace MaxFlowGraph {
|
||||
const int INF = std::numeric_limits<int>::max();
|
||||
const int INVALID_ID = -1;
|
||||
// Upper bound for MCMF edge cost to prevent int overflow in SPFA causing infinite loops
|
||||
constexpr int MCMF_MAX_EDGE_COST = 10000000;
|
||||
}
|
||||
|
||||
// Namespace-scope edge shared by the max-flow / min-cost-max-flow solvers below.
|
||||
// The default cost keeps the plain max-flow solvers (which never read cost) source-compatible.
|
||||
struct Edge
|
||||
{
|
||||
int from, to, capacity, cost, flow;
|
||||
Edge(int u, int v, int cap, int cst = 0) : from(u), to(v), capacity(cap), cost(cst), flow(0) {}
|
||||
};
|
||||
|
||||
class MaxFlowSolver
|
||||
{
|
||||
private:
|
||||
struct Edge {
|
||||
int from, to, capacity, flow;
|
||||
Edge(int u, int v, int cap) :from(u), to(v), capacity(cap), flow(0) {}
|
||||
};
|
||||
public:
|
||||
MaxFlowSolver(const std::vector<int>& u_nodes, const std::vector<int>& v_nodes,
|
||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
||||
const std::vector<int>& u_capacity = {},
|
||||
const std::vector<int>& v_capacity = {}
|
||||
const std::vector<int>& v_capacity = {},
|
||||
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
|
||||
);
|
||||
std::vector<int> solve();
|
||||
|
||||
@@ -47,6 +56,7 @@ private:
|
||||
|
||||
|
||||
struct MinCostMaxFlow;
|
||||
struct MaxFlowWithLowerBounds;
|
||||
|
||||
class GeneralMinCostSolver
|
||||
{
|
||||
@@ -61,6 +71,84 @@ private:
|
||||
std::unique_ptr<MinCostMaxFlow> m_solver;
|
||||
};
|
||||
|
||||
class GeneralMinCostLowerBoundsSolver
|
||||
{
|
||||
public:
|
||||
GeneralMinCostLowerBoundsSolver(
|
||||
const std::vector<FlushMatrix> &matrix_,
|
||||
const std::vector<int>& u_nodes,
|
||||
const std::vector<int>& v_nodes,
|
||||
const std::vector<int>& v_nodes_group,
|
||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {});
|
||||
|
||||
std::vector<int> solve();
|
||||
~GeneralMinCostLowerBoundsSolver();
|
||||
|
||||
private:
|
||||
void build_feasible_graph(const std::unordered_set<int>& no_lower_groups);
|
||||
|
||||
void build_graph_with_feasible_result();
|
||||
|
||||
void add_edge_with_lower_bound(int from, int to, int lower, int upper, int cost);
|
||||
|
||||
int get_distance(const int idx_in_left,const int idx_in_right);
|
||||
|
||||
private:
|
||||
std::unique_ptr<MaxFlowWithLowerBounds> m_solver_lower_bounds;
|
||||
std::unique_ptr<MinCostMaxFlow> m_solver_min_cost;
|
||||
|
||||
std::vector<FlushMatrix> flush_matrix;
|
||||
std::vector<int> l_nodes;
|
||||
std::vector<int> r_nodes;
|
||||
std::vector<int> r_nodes_group;
|
||||
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
|
||||
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
|
||||
int num_groups = 0;
|
||||
|
||||
// support lower bounds
|
||||
struct LowerBoundEdge{
|
||||
int edge_id;
|
||||
int lower;
|
||||
};
|
||||
|
||||
std::vector<int> demand;
|
||||
std::vector<LowerBoundEdge> lower_bound_edges;
|
||||
|
||||
int super_source = -1;
|
||||
int super_sink = -1;
|
||||
int source_id = -1;
|
||||
int sink_id = -1;
|
||||
int max_flow_edges = 0;
|
||||
};
|
||||
|
||||
class GroupMinCostFlowSolver
|
||||
{
|
||||
public:
|
||||
GroupMinCostFlowSolver(
|
||||
const std::vector<FlushMatrix> &matrix_,
|
||||
const std::vector<int> &u_nodes,
|
||||
const std::vector<int> &v_nodes,
|
||||
const std::vector<int> &v_nodes_group,
|
||||
const std::unordered_map<int, std::vector<int>> &uv_link_limits = {},
|
||||
const std::unordered_map<int, std::vector<int>> &uv_unlink_limits = {});
|
||||
|
||||
std::vector<int> solve();
|
||||
~GroupMinCostFlowSolver();
|
||||
|
||||
private:
|
||||
void build_graph();
|
||||
int get_flush_cost(int l_idx, int r_idx);
|
||||
|
||||
std::unique_ptr<MinCostMaxFlow> m_solver;
|
||||
std::vector<FlushMatrix> flush_matrix;
|
||||
std::vector<int> l_nodes;
|
||||
std::vector<int> r_nodes;
|
||||
std::vector<int> r_nodes_group;
|
||||
std::unordered_map<int, std::vector<int>> m_uv_link_limits;
|
||||
std::unordered_map<int, std::vector<int>> m_uv_unlink_limits;
|
||||
int num_groups = 0;
|
||||
};
|
||||
|
||||
class MinFlushFlowSolver
|
||||
{
|
||||
@@ -71,7 +159,8 @@ public:
|
||||
const std::unordered_map<int, std::vector<int>>& uv_link_limits = {},
|
||||
const std::unordered_map<int, std::vector<int>>& uv_unlink_limits = {},
|
||||
const std::vector<int>& u_capacity = {},
|
||||
const std::vector<int>& v_capacity = {}
|
||||
const std::vector<int>& v_capacity = {},
|
||||
const std::vector<std::pair<std::set<int>, int>>& v_group_capacity = {}
|
||||
);
|
||||
std::vector<int> solve();
|
||||
~MinFlushFlowSolver();
|
||||
@@ -108,7 +197,19 @@ int reorder_filaments_for_minimum_flush_volume(const std::vector<unsigned int> &
|
||||
const std::vector<std::vector<unsigned int>> &layer_filaments,
|
||||
const std::vector<FlushMatrix> &flush_matrix,
|
||||
std::optional<std::function<bool(int, std::vector<int> &)>> get_custom_seq,
|
||||
std::vector<std::vector<unsigned int>> *filament_sequences);
|
||||
std::vector<std::vector<unsigned int>> *filament_sequences,
|
||||
const std::unordered_map<int, int>& nozzle_status = {});
|
||||
|
||||
// Order filaments within a per-nozzle grouping result (multi-nozzle extruders). Threads a
|
||||
// NozzleStatusRecorder describing the initial physical nozzle occupancy so the reorder can reward
|
||||
// keeping an already-loaded filament in place.
|
||||
int reorder_filaments_for_multi_nozzle_extruder(const std::vector<unsigned int>& filament_lists,
|
||||
const MultiNozzleUtils::LayeredNozzleGroupResult& nozzle_group_result,
|
||||
const std::vector<std::vector<unsigned int>>& layer_filaments,
|
||||
const std::vector<FlushMatrix>& flush_matrix,
|
||||
const std::function<bool(int,std::vector<int>&)> get_custom_seq,
|
||||
std::vector<std::vector<unsigned int>> * filament_sequences,
|
||||
const MultiNozzleUtils::NozzleStatusRecorder& initial_status = {});
|
||||
|
||||
}
|
||||
#endif // !TOOL_ORDER_UTILS_HPP
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#include "../FilamentGroup.hpp"
|
||||
#include "../MultiNozzleUtils.hpp"
|
||||
#include "../ExtrusionEntity.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
|
||||
@@ -98,19 +99,23 @@ private:
|
||||
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};
|
||||
int extruder_change_count{0};
|
||||
|
||||
void clear(){
|
||||
filament_flush_weight = 0;
|
||||
filament_change_count = 0;
|
||||
extruder_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->extruder_change_count += other.extruder_change_count;
|
||||
this->flush_filament_change_count += other.flush_filament_change_count;
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -118,7 +123,7 @@ struct FilamentChangeStats
|
||||
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.extruder_change_count = this->extruder_change_count + other.extruder_change_count;
|
||||
ret.flush_filament_change_count = this->flush_filament_change_count + other.flush_filament_change_count;
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -236,12 +241,21 @@ public:
|
||||
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; }
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
static std::vector<int> 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);
|
||||
// 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 = {});
|
||||
|
||||
// should be called after doing reorder
|
||||
FilamentChangeStats get_filament_change_stats(FilamentChangeMode mode);
|
||||
@@ -283,6 +297,13 @@ private:
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
#include <iomanip>
|
||||
@@ -1493,6 +1494,21 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
|
||||
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value)
|
||||
{
|
||||
m_flat_ironing = (m_flat_ironing && m_use_gap_wall);
|
||||
|
||||
// Prime-tower heating during wipe. m_is_multiple_nozzle mirrors the gate used in ToolOrdering/GCode
|
||||
// (std::any_of extruder_max_nozzle_count > 1); it is false for every current printer, so the
|
||||
// heating-during-wipe logic in toolchange_wipe_new is inert.
|
||||
m_hotend_heating_rate = config.hotend_heating_rate.values;
|
||||
m_physical_extruder_map = config.physical_extruder_map.values;
|
||||
m_is_multiple_nozzle = std::any_of(config.extruder_max_nozzle_count.values.begin(),
|
||||
config.extruder_max_nozzle_count.values.end(),
|
||||
[](int v) { return v > 1; });
|
||||
|
||||
// Per-extruder printable-height clamp. Empty for single-extruder printers
|
||||
// (extruder_printable_height = []), so is_valid_last_layer is inert there.
|
||||
m_printable_height = config.extruder_printable_height.values;
|
||||
m_last_layer_id.assign(config.nozzle_diameter.size(), -1);
|
||||
|
||||
// Read absolute value of first layer speed, if given as percentage,
|
||||
// it is taken over following default. Speeds from config are not
|
||||
// easily accessible here.
|
||||
@@ -1558,8 +1574,12 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
|
||||
}
|
||||
m_filpar[idx].tower_interface_pre_extrusion_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
|
||||
m_filpar[idx].tower_interface_pre_extrusion_length = config.filament_tower_interface_pre_extrusion_length.get_at(idx);
|
||||
// PETG pre-extrusion offset reuses the tower-interface pre-extrusion distance. Only read by the
|
||||
// has_filament_switcher-gated PETG branch in get_next_pos (inert fleet-wide).
|
||||
m_filpar[idx].petg_pre_extrusion_offset_dist = config.filament_tower_interface_pre_extrusion_dist.get_at(idx);
|
||||
m_filpar[idx].tower_ironing_area = config.filament_tower_ironing_area.get_at(idx);
|
||||
m_filpar[idx].tower_interface_purge_length = config.filament_tower_interface_purge_volume.get_at(idx);
|
||||
m_filpar[idx].filament_cooling_before_tower = config.filament_cooling_before_tower.get_at(idx);
|
||||
|
||||
// If this is a single extruder MM printer, we will use all the SE-specific config values.
|
||||
// Otherwise, the defaults will be used to turn off the SE stuff.
|
||||
@@ -1651,6 +1671,27 @@ Vec2f WipeTower::get_next_pos(const WipeTower::box_coordinates &cleaning_box, fl
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
// Shift the wipe start outward for a PETG pre-extrusion on filament-switcher devices, clamped to the
|
||||
// shared printable bed. Gated on m_has_filament_switcher, which is false for the whole shipping fleet
|
||||
// (no profile sets the key), so is_petg_pre_extrusion is always false and res is returned unchanged.
|
||||
// The tower-interface contact branch is deliberately NOT applied here (enable_tower_interface_features
|
||||
// DOES ship on H2C/X2D; applying it would change their g-code); is_contact_pre_extrusion is computed
|
||||
// only as the guard that gives the contact path priority over PETG.
|
||||
bool is_contact_pre_extrusion = interface_layer && m_enable_tower_interface_features;
|
||||
bool is_petg_pre_extrusion = !is_contact_pre_extrusion && is_petg_filament(m_current_tool) && m_has_filament_switcher;
|
||||
if (is_petg_pre_extrusion) {
|
||||
Vec2f stop_pos = res;
|
||||
float offset_dist = m_filpar[m_current_tool].petg_pre_extrusion_offset_dist;
|
||||
auto printer_bbx = unscaled(get_extents(m_shared_print_bed)); // BoundingBoxBase<Vec2d>
|
||||
printer_bbx.translate((-m_wipe_tower_pos - m_rib_offset).cast<double>());
|
||||
if (stop_pos.x() < m_wipe_tower_width / 2.f)
|
||||
stop_pos = Vec2f(stop_pos.x() - offset_dist, stop_pos.y());
|
||||
else
|
||||
stop_pos = Vec2f(stop_pos.x() + offset_dist, stop_pos.y());
|
||||
if (stop_pos.x() < printer_bbx.min[0]) stop_pos.x() = printer_bbx.min[0];
|
||||
if (stop_pos.x() > printer_bbx.max[0]) stop_pos.x() = printer_bbx.max[0];
|
||||
res = stop_pos;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -2645,6 +2686,11 @@ bool WipeTower::is_tpu_filament(int filament_id) const
|
||||
return m_filpar[filament_id].material == "TPU";
|
||||
}
|
||||
|
||||
bool WipeTower::is_petg_filament(int filament_id) const
|
||||
{
|
||||
return m_filpar[filament_id].material == "PETG";
|
||||
}
|
||||
|
||||
// BBS: consider both soluable and support properties
|
||||
// Return index of first toolchange that switches to non-soluble and non-support extruder
|
||||
// ot -1 if there is no such toolchange.
|
||||
@@ -2717,6 +2763,9 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
|
||||
float spacing = m_layer_info->extra_spacing;
|
||||
if (has_tpu_filament() && m_layer_info->extra_spacing < m_tpu_fixed_spacing) spacing = 1;
|
||||
float nozzle_change_depth = tool_change.nozzle_change_depth * spacing;
|
||||
// Drop the nozzle-change depth on an extruder's final layer above its printable height
|
||||
// (inert unless is_valid_last_layer clamps, i.e. multi-extruder near Z-max).
|
||||
if (!is_valid_last_layer(old_filament, m_cur_layer_id, layer.z)) nozzle_change_depth = 0.f;
|
||||
//float nozzle_change_depth = tool_change.nozzle_change_depth * (has_tpu_filament() ? m_tpu_fixed_spacing : layer.extra_spacing);
|
||||
auto* block = get_block_by_category(m_filpar[new_filament].category, false);
|
||||
if (!block)
|
||||
@@ -2760,7 +2809,10 @@ void WipeTower::get_wall_skip_points(const WipeTowerInfo &layer)
|
||||
WipeTower::ToolChangeResult WipeTower::tool_change_new(size_t new_tool, bool solid_toolchange,bool solid_nozzlechange)
|
||||
{
|
||||
m_nozzle_change_result.gcode.clear();
|
||||
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]) {
|
||||
// Skip the cross-extruder nozzle change (ramming) on an extruder's final layer above its printable
|
||||
// height. is_valid_last_layer is inert unless multi-extruder near Z-max.
|
||||
if (!m_filament_map.empty() && new_tool < m_filament_map.size() && m_filament_map[m_current_tool] != m_filament_map[new_tool]
|
||||
&& is_valid_last_layer(m_current_tool, m_cur_layer_id, m_z_pos)) {
|
||||
m_nozzle_change_result = nozzle_change_new(m_current_tool, new_tool, solid_nozzlechange);
|
||||
}
|
||||
|
||||
@@ -3411,23 +3463,103 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
||||
x_to_wipe = solid_tool_toolchange ? std::numeric_limits<float>::max(): x_to_wipe;
|
||||
float target_speed = is_first_layer() ? std::min(m_first_layer_speed * 60.f, 4800.f) : 4800.f;
|
||||
target_speed = solid_tool_toolchange ? 20.f * 60.f : target_speed;
|
||||
float wipe_speed = 0.33f * target_speed;
|
||||
// Nominal wipe-speed schedule. The applied wipe_speed is nominal_speed * speed_factor; speed_factor
|
||||
// stays 1.0 unless the H2C prime-tower heating-during-wipe model below slows the wipe so the hotend
|
||||
// can reach temperature (nominal_speed == wipe_speed when speed_factor == 1, i.e. single-nozzle).
|
||||
float nominal_speed = 0.33f * target_speed;
|
||||
|
||||
m_left_to_right = ((m_cur_layer_id + 3) % 4 >= 2);
|
||||
|
||||
bool is_from_up = (m_cur_layer_id % 2 == 1);
|
||||
|
||||
// Prime-tower heating during wipe. Everything here is gated on m_is_multiple_nozzle (false for every
|
||||
// current printer); the lambdas emit nothing until add_M104_by_requirement's gate opens, so the
|
||||
// single-nozzle wipe is untouched.
|
||||
// WipeSpeedMap mirrors the nominal schedule above and is read only by estimate_wipe_time. It is a
|
||||
// std::array (stack, no per-call heap allocation); values depend on runtime target_speed so it
|
||||
// cannot be static const.
|
||||
const std::array<float, 5> WipeSpeedMap{0.33f * target_speed, 0.375f * target_speed, 0.458f * target_speed,
|
||||
0.875f * target_speed, std::min(target_speed, 0.875f * target_speed + 50.f)};
|
||||
auto estimate_wipe_time = [&cleaning_box, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange]() -> float {
|
||||
int n = std::ceil(x_to_wipe / (xr - xl));
|
||||
if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy;
|
||||
float one_line_len = xr - xl;
|
||||
float time = std::numeric_limits<float>::max();
|
||||
if (n <= 1)
|
||||
time = one_line_len / WipeSpeedMap[0];
|
||||
else if (n <= 2)
|
||||
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1];
|
||||
else if (n <= 3)
|
||||
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2];
|
||||
else if (n <= 4)
|
||||
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
|
||||
else {
|
||||
time = one_line_len / WipeSpeedMap[0] + one_line_len / WipeSpeedMap[1] + one_line_len / WipeSpeedMap[2] + one_line_len / WipeSpeedMap[3];
|
||||
time += (n - 4) * one_line_len / WipeSpeedMap[4];
|
||||
}
|
||||
return time * 60.f;
|
||||
};
|
||||
// Emit the arriving-hotend pre-heat inside the M632/M633 nozzle-change barrier. `M632 S<tool>[ H<nozzle>]
|
||||
// M N` opens the barrier (M = firmware nozzle-change flag, N = slicer generated), the M104 sets the
|
||||
// arriving hotend temp, and `M633` closes it. H2C's grouping is static (no dynamic nozzle map), so the
|
||||
// H<nozzle> field is omitted (a dynamic nozzle map would supply a real nozzle id, a static map -1 => no
|
||||
// H). The counterproductive fan-on (M106 S255) used for departing-tool cooldown is intentionally
|
||||
// omitted, since this is a pre-HEAT of the arriving tool. The whole helper is only ever called from
|
||||
// add_M104_by_requirement, which is gated on m_is_multiple_nozzle (extruder_max_nozzle_count>1) => H2C
|
||||
// only; every other printer's wipe tower is untouched. The M632 M-flag is itself a firmware barrier, so
|
||||
// a preceding M400 wait is subsumed.
|
||||
auto format_line_M104 = [this](int target_temp, int target_extruder = -1, bool wait_for_moves = true, const std::string &comment = "") {
|
||||
std::string buffer;
|
||||
buffer += "M632 S" + std::to_string(m_current_tool) + " M N\n";
|
||||
buffer += "M104";
|
||||
if (target_extruder != -1 && target_extruder < (int) m_physical_extruder_map.size())
|
||||
buffer += (" T" + std::to_string(m_physical_extruder_map[target_extruder]));
|
||||
buffer += " S" + std::to_string(target_temp) + " N0"; // N0 means the gcode is generated by the slicer
|
||||
if (!comment.empty()) buffer += " ;" + comment;
|
||||
buffer += '\n';
|
||||
buffer += "M633\n";
|
||||
(void) wait_for_moves; // the M632 M-flag barrier replaces the former M400 wait
|
||||
return buffer;
|
||||
};
|
||||
// Suppress the pre-heat M104 on the first layer and on solid (contact) toolchanges (should_heating).
|
||||
// m_is_multiple_nozzle folds in the H2C gate so single-nozzle output is untouched.
|
||||
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (layer-static) because Orca's
|
||||
// wipe tower is extruder-level rather than tracking a per-layer nozzle map.
|
||||
bool should_heating = m_is_multiple_nozzle && m_filpar[m_current_tool].filament_cooling_before_tower > EPSILON &&
|
||||
!solid_tool_toolchange && !is_first_layer();
|
||||
auto add_M104_by_requirement = [&writer, &format_line_M104, &should_heating, this]() {
|
||||
if (m_filpar[m_current_tool].filament_cooling_before_tower < EPSILON) return;
|
||||
if (!should_heating) return;
|
||||
float target_temp = is_first_layer() ? m_filpar[m_current_tool].nozzle_temperature_initial_layer : m_filpar[m_current_tool].nozzle_temperature;
|
||||
writer.append(format_line_M104(target_temp, m_filament_map[m_current_tool] - 1));
|
||||
};
|
||||
float speed_factor = 1.f;
|
||||
if (should_heating) {
|
||||
// The heating-slowdown scaling is disabled — no additional heating time is required, so
|
||||
// speed_factor stays 1.0. The structure and estimate_wipe_time/WipeSpeedMap are retained for
|
||||
// future H2C tuning; the divide-by-zero/bounds guard is preserved in the commented body below.
|
||||
// int extruder_id = m_filament_map[m_current_tool] - 1;
|
||||
// if (extruder_id >= 0 && extruder_id < (int) m_hotend_heating_rate.size() && m_hotend_heating_rate[extruder_id] > 0.) {
|
||||
// float estimate_time = estimate_wipe_time();
|
||||
// float heat_time = m_filpar[m_current_tool].filament_cooling_before_tower / m_hotend_heating_rate[extruder_id];
|
||||
// if (estimate_time < heat_time) speed_factor = estimate_time / heat_time;
|
||||
// }
|
||||
(void) estimate_wipe_time; // retain scaffolding above without an unused-lambda warning
|
||||
}
|
||||
float wipe_speed = nominal_speed * speed_factor;
|
||||
|
||||
// now the wiping itself:
|
||||
for (int i = 0; true; ++i) {
|
||||
if (i != 0) {
|
||||
if (wipe_speed < 0.34f * target_speed)
|
||||
wipe_speed = 0.375f * target_speed;
|
||||
else if (wipe_speed < 0.377 * target_speed)
|
||||
wipe_speed = 0.458f * target_speed;
|
||||
else if (wipe_speed < 0.46f * target_speed)
|
||||
wipe_speed = 0.875f * target_speed;
|
||||
if (nominal_speed < 0.34f * target_speed)
|
||||
nominal_speed = 0.375f * target_speed;
|
||||
else if (nominal_speed < 0.377 * target_speed)
|
||||
nominal_speed = 0.458f * target_speed;
|
||||
else if (nominal_speed < 0.46f * target_speed)
|
||||
nominal_speed = 0.875f * target_speed;
|
||||
else
|
||||
wipe_speed = std::min(target_speed, wipe_speed + 50.f);
|
||||
nominal_speed = std::min(target_speed, nominal_speed + 50.f);
|
||||
wipe_speed = nominal_speed * speed_factor;
|
||||
}
|
||||
|
||||
bool need_change_flow = need_thick_bridge_flow(writer.y());
|
||||
@@ -3453,6 +3585,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
||||
} else
|
||||
writer.travel(writer.x() + 1.5 * ironing_length, writer.y(), 240.);
|
||||
writer.retract(-retract_length, retract_speed);
|
||||
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||
} else {
|
||||
float dx = xl - wipe_tower_wall_infill_overlap * m_perimeter_width - writer.pos().x();
|
||||
@@ -3468,9 +3601,11 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
||||
}else
|
||||
writer.travel(writer.x() - 1.5 * ironing_length, writer.y(), 240.);
|
||||
writer.retract(-retract_length, retract_speed);
|
||||
add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||
writer.extrude(xl - wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||
}
|
||||
} else {
|
||||
if (i == 0) add_M104_by_requirement(); // Pre-heat the arriving hotend during the wipe
|
||||
if (m_left_to_right)
|
||||
writer.extrude(xr + wipe_tower_wall_infill_overlap * m_perimeter_width, writer.y(), wipe_speed);
|
||||
else
|
||||
@@ -3571,6 +3706,47 @@ bool WipeTower::is_in_same_extruder(int filament_id_1, int filament_id_2)
|
||||
return m_filament_map[filament_id_1] == m_filament_map[filament_id_2];
|
||||
}
|
||||
|
||||
// Per-extruder printable-height clamp: is an extruder still allowed to print on this wipe-tower layer,
|
||||
// or is it its final layer above the extruder's printable height?
|
||||
// Orca: the arriving extruder id is resolved as m_filament_map[tool]-1 (1-based map, layer-static),
|
||||
// because Orca's wipe tower is extruder-level rather than tracking a per-layer nozzle map (the same
|
||||
// idiom the pre-heat path uses in toolchange_wipe_new). Gated on m_is_multi_extruder so that
|
||||
// single-extruder printers (including ones whose extruder_printable_height defaults to {0}) always
|
||||
// return true and leave wipe-tower g-code unchanged.
|
||||
bool WipeTower::is_valid_last_layer(int tool, int layer_id, double layer_z) const
|
||||
{
|
||||
if (!m_is_multi_extruder)
|
||||
return true;
|
||||
int extruder_id = (tool >= 0 && tool < (int) m_filament_map.size()) ? m_filament_map[tool] - 1 : -1;
|
||||
if (extruder_id < 0 || extruder_id >= (int) m_printable_height.size() || extruder_id >= (int) m_last_layer_id.size())
|
||||
return true;
|
||||
if (m_last_layer_id[extruder_id] == layer_id && layer_z > m_printable_height[extruder_id])
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Records, per extruder, the last wipe-tower layer index that uses it, so is_valid_last_layer can
|
||||
// recognise the extruder's final layer. Inert for single-extruder printers (early return);
|
||||
// bounds-checked because m_filament_map may be empty/short.
|
||||
void WipeTower::set_nozzle_last_layer_id()
|
||||
{
|
||||
if (!m_is_multi_extruder)
|
||||
return;
|
||||
for (int idx = 0; idx < (int) m_plan.size(); ++idx) {
|
||||
const auto &info = m_plan[idx];
|
||||
for (const auto &tc : info.tool_changes) {
|
||||
int old_tool = (int) tc.old_tool;
|
||||
int new_tool = (int) tc.new_tool;
|
||||
int old_ext = (old_tool >= 0 && old_tool < (int) m_filament_map.size()) ? m_filament_map[old_tool] - 1 : -1;
|
||||
int new_ext = (new_tool >= 0 && new_tool < (int) m_filament_map.size()) ? m_filament_map[new_tool] - 1 : -1;
|
||||
if (old_ext >= 0 && old_ext < (int) m_last_layer_id.size())
|
||||
m_last_layer_id[old_ext] = idx;
|
||||
if (new_ext >= 0 && new_ext < (int) m_last_layer_id.size())
|
||||
m_last_layer_id[new_ext] = idx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WipeTower::reset_block_status()
|
||||
{
|
||||
for (auto &block : m_wipe_tower_blocks) {
|
||||
@@ -3797,6 +3973,7 @@ void WipeTower::plan_tower_new()
|
||||
}
|
||||
|
||||
update_all_layer_depth(max_depth);
|
||||
set_nozzle_last_layer_id(); // record per-extruder last layer for is_valid_last_layer
|
||||
float diagonal = sqrt(m_wipe_tower_depth * m_wipe_tower_depth + m_wipe_tower_width * m_wipe_tower_width);
|
||||
m_rib_length = std::max({m_rib_length, diagonal});
|
||||
m_rib_length += m_extra_rib_length;
|
||||
@@ -3916,9 +4093,12 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
||||
int candidate_id = -1;
|
||||
for (size_t idx = 0; idx < layer.tool_changes.size(); ++idx) {
|
||||
if (idx == 0) {
|
||||
if (layer.tool_changes[idx].old_tool == wall_filament_id)
|
||||
// An extruder's last-layer filament above its printable height cannot supply the
|
||||
// outer wall. is_valid_last_layer is inert unless it clamps.
|
||||
if (layer.tool_changes[idx].old_tool == wall_filament_id && is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z))
|
||||
return wall_filament_id;
|
||||
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category) {
|
||||
else if (m_filpar[layer.tool_changes[idx].old_tool].category == m_filpar[wall_filament_id].category &&
|
||||
is_valid_last_layer(layer.tool_changes[idx].old_tool, m_cur_layer_id, layer.z)) {
|
||||
candidate_id = layer.tool_changes[idx].old_tool;
|
||||
}
|
||||
}
|
||||
@@ -4017,6 +4197,10 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
||||
finish_layer_filament = wall_idx;
|
||||
}
|
||||
|
||||
// Cancel a block on the last layer above its extruder's printable height.
|
||||
// is_valid_last_layer is inert unless multi-extruder near Z-max.
|
||||
if (!is_valid_last_layer(finish_layer_filament, m_cur_layer_id, layer.z)) continue;
|
||||
|
||||
ToolChangeResult finish_block_tcr;
|
||||
if (interface_solid || (block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament])) {
|
||||
interface_solid = interface_solid && !((block.solid_infill[m_cur_layer_id] && block.filament_adhesiveness_category != m_filament_categories[finish_layer_filament]));//noly reduce speed when
|
||||
|
||||
@@ -313,6 +313,13 @@ public:
|
||||
void set_has_tpu_filament(bool has_tpu) { m_has_tpu_filament = has_tpu; }
|
||||
bool has_tpu_filament() const { return m_has_tpu_filament; }
|
||||
|
||||
// Orca: has_filament_switcher is not a static PrintConfig member, so it is pushed in from Print
|
||||
// via a setter rather than read in the ctor. Device-set only.
|
||||
void set_has_filament_switcher(bool v) { m_has_filament_switcher = v; }
|
||||
// The region every extruder can reach, used to clamp the PETG pre-extrusion offset to the
|
||||
// printable bed.
|
||||
void set_shared_print_bed(const Polygons &bed) { m_shared_print_bed = bed; }
|
||||
|
||||
struct FilamentParameters {
|
||||
std::string material = "PLA";
|
||||
int category;
|
||||
@@ -341,8 +348,14 @@ public:
|
||||
float wipe_dist;
|
||||
float tower_interface_pre_extrusion_dist = 0.f;
|
||||
float tower_interface_pre_extrusion_length = 0.f;
|
||||
// Outward shift of the wipe start for a PETG pre-extrusion on filament-switcher devices;
|
||||
// set from filament_tower_interface_pre_extrusion_dist.
|
||||
float petg_pre_extrusion_offset_dist = 0.f;
|
||||
float tower_ironing_area = 4.f;
|
||||
float tower_interface_purge_length = 0.f;
|
||||
// Distance (in mm of filament) that a hotend is allowed to pre-cool before the
|
||||
// tower is reached; drives the prime-tower heating-during-wipe model (multi-nozzle only).
|
||||
float filament_cooling_before_tower = 0.f;
|
||||
};
|
||||
|
||||
|
||||
@@ -462,6 +475,22 @@ private:
|
||||
bool m_adhesion = true;
|
||||
GCodeFlavor m_gcode_flavor;
|
||||
|
||||
// Multi-nozzle prime-tower heating during wipe. m_is_multiple_nozzle gates the whole
|
||||
// feature; it is false for every current (single-nozzle) printer (extruder_max_nozzle_count
|
||||
// defaults to 1), so the pre-heat/pre-cool path is inert and wipe-tower g-code is unchanged.
|
||||
bool m_is_multiple_nozzle = false;
|
||||
std::vector<double> m_hotend_heating_rate; // config.hotend_heating_rate (deg/s per extruder)
|
||||
std::vector<int> m_physical_extruder_map; // logical extruder -> physical tool number (M104 T param)
|
||||
|
||||
// Per-extruder printable-height clamp. m_printable_height = config.extruder_printable_height
|
||||
// (per-extruder Z limit; empty for single-extruder printers, [320,325] for H2D). m_last_layer_id
|
||||
// records, per extruder, the last wipe-tower layer that uses it. is_valid_last_layer() is gated on
|
||||
// m_is_multi_extruder so single-extruder wipe-tower g-code is unchanged; the clamp only bites a
|
||||
// multi-extruder wipe tower whose final per-extruder layer exceeds that extruder's printable
|
||||
// height (near the Z limit).
|
||||
std::vector<double> m_printable_height;
|
||||
std::vector<int> m_last_layer_id;
|
||||
|
||||
// Bed properties
|
||||
enum {
|
||||
RectangularBed,
|
||||
@@ -501,6 +530,11 @@ private:
|
||||
bool m_flat_ironing=false;
|
||||
bool m_enable_tower_interface_features=false;
|
||||
bool m_enable_tower_interface_cooldown_during_tower=false;
|
||||
// Filament-switcher device flag + shared printable bed for the PETG pre-extrusion offset.
|
||||
// m_has_filament_switcher is false for the whole shipping fleet (no profile sets the key), so
|
||||
// the PETG branch in get_next_pos never runs -> no change fleet-wide.
|
||||
bool m_has_filament_switcher=false;
|
||||
Polygons m_shared_print_bed;
|
||||
bool m_prev_layer_had_interface=false;
|
||||
bool m_current_layer_has_interface=false;
|
||||
// Calculates length of extrusion line to extrude given volume
|
||||
@@ -520,6 +554,7 @@ private:
|
||||
void save_on_last_wipe();
|
||||
|
||||
bool is_tpu_filament(int filament_id) const;
|
||||
bool is_petg_filament(int filament_id) const;
|
||||
|
||||
// BBS
|
||||
box_coordinates align_perimeter(const box_coordinates& perimeter_box);
|
||||
@@ -586,6 +621,12 @@ private:
|
||||
const box_coordinates &cleaning_box,
|
||||
float wipe_volume);
|
||||
void get_wall_skip_points(const WipeTowerInfo &layer);
|
||||
|
||||
// Per-extruder printable-height clamp (see m_printable_height). is_valid_last_layer returns
|
||||
// false only for a multi-extruder wipe tower's final per-extruder layer that exceeds that
|
||||
// extruder's printable height; returns true (no clamp) in every other case.
|
||||
bool is_valid_last_layer(int tool, int layer_id, double layer_z) const;
|
||||
void set_nozzle_last_layer_id();
|
||||
};
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user