Size the prime tower from the actual flush volumes (#15149)

# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

The prime tower reserved its footprint from the prime volume alone,
ignoring the flush volumes it actually has to hold, so on a multi-colour
print the tower shown in Prepare and the space kept clear for it during
arrange could be far smaller than the tower that gets sliced — leaving
it overlapping objects or running off the plate. This sizes the estimate
from the configured flush volumes instead, for rib walls as well as
rectangle and cone, applies the same height-based minimum depth the
prime-volume estimate already used, and reads the flush matrix correctly
on multi-nozzle printers, where it holds one block per nozzle.

Only the pre-slice estimate changes: the generated tower is untouched,
and prints that do not purge into the prime tower keep their existing
size.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
SoftFever
2026-08-06 18:17:05 +08:00
committed by GitHub
5 changed files with 83 additions and 41 deletions

View File

@@ -2130,31 +2130,68 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
} }
// Static method to extract wipe_volumes[from][to] from the configuration. // Static method to extract wipe_volumes[from][to] from the configuration.
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const PrintConfig& config) // Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const ConfigBase& config)
{ {
// Get wiping matrix to get number of extruders and convert vector<double> to vector<float>: // flush_volumes_matrix holds one filaments x filaments block per nozzle (written by
std::vector<float> wiping_matrix(cast<float>(config.flush_volumes_matrix.values)); // PresetBundle::update_multi_material_filament_presets), so the filament count is
auto scale = config.flush_multiplier.get_at(0); // sqrt(size / nozzles). One tower serves every nozzle and the filament to nozzle assignment is
// only decided later by ToolOrdering, so fold the blocks with std::max: the depth reserved here
// has to cover the worst nozzle. With a single nozzle the fold has one term.
const std::vector<double> &raw_matrix = config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
const auto *nozzle_diameter = config.option<ConfigOptionFloats>("nozzle_diameter");
size_t nozzle_nums = (nozzle_diameter == nullptr || nozzle_diameter->values.empty()) ? 1 : nozzle_diameter->values.size();
unsigned int number_of_extruders = (unsigned int)(sqrt(raw_matrix.size() / nozzle_nums) + EPSILON);
if (size_t(number_of_extruders) * number_of_extruders * nozzle_nums != raw_matrix.size()) {
// Saved for a different nozzle count (older project, or the printer was just switched):
// fall back to reading the whole option as one block, as this did before.
nozzle_nums = 1;
number_of_extruders = (unsigned int)(sqrt(raw_matrix.size()) + EPSILON);
}
// The values shall only be used when SEMM is enabled. The purging for other printers // The values shall only be used when SEMM is enabled. The purging for other printers
// is determined by filament_minimal_purge_on_wipe_tower. // is determined by filament_minimal_purge_on_wipe_tower.
if (! config.purge_in_prime_tower.value || ! config.single_extruder_multi_material.value) const bool purge = config.option<ConfigOptionBool>("purge_in_prime_tower")->value
std::fill(wiping_matrix.begin(), wiping_matrix.end(), 0.f); && config.option<ConfigOptionBool>("single_extruder_multi_material")->value;
// Extract purging volumes for each extruder pair: // Extract purging volumes for each extruder pair, each nozzle's block scaled by its own multiplier:
std::vector<std::vector<float>> wipe_volumes; std::vector<std::vector<float>> wipe_volumes(number_of_extruders, std::vector<float>(number_of_extruders, 0.f));
const unsigned int number_of_extruders = (unsigned int)(sqrt(wiping_matrix.size())+EPSILON); if (purge) {
for (size_t i = 0; i<number_of_extruders; ++i) const auto *multiplier = config.option<ConfigOptionFloats>("flush_multiplier");
wipe_volumes.push_back(std::vector<float>(wiping_matrix.begin()+i*number_of_extruders, wiping_matrix.begin()+(i+1)*number_of_extruders)); for (size_t nozzle_id = 0; nozzle_id < nozzle_nums; ++nozzle_id) {
const std::vector<double> block = get_flush_volumes_matrix(raw_matrix, nozzle_id, nozzle_nums);
const double scale = multiplier->get_at(nozzle_id);
for (unsigned int i = 0; i<number_of_extruders; ++i)
for (unsigned int j = 0; j<number_of_extruders; ++j)
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], float(block[size_t(i) * number_of_extruders + j]) * scale);
}
}
// Also include filament_minimal_purge_on_wipe_tower. This is needed for the preview. // Also include filament_minimal_purge_on_wipe_tower. This is needed for the preview.
const auto *minimal_purge = config.option<ConfigOptionFloats>("filament_minimal_purge_on_wipe_tower");
for (unsigned int i = 0; i<number_of_extruders; ++i) for (unsigned int i = 0; i<number_of_extruders; ++i)
for (unsigned int j = 0; j<number_of_extruders; ++j) for (unsigned int j = 0; j<number_of_extruders; ++j)
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j] * scale, config.filament_minimal_purge_on_wipe_tower.get_at(j)); wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j], minimal_purge->get_at(j));
return wipe_volumes; return wipe_volumes;
} }
float WipeTower2::estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt)
{
const std::vector<std::vector<float>> wipe_volumes = extract_wipe_volumes(config);
if (wipe_volumes.empty()) // an empty flush matrix would make the average below 0/0
return 0.f;
float maximum = 0.f;
for (const std::vector<float> &v : wipe_volumes)
maximum += *std::max_element(v.begin(), v.end());
maximum = maximum * filaments_cnt / wipe_volumes.size();
// Orca: it's overshooting a bit, so let's reduce it a bit
maximum *= 0.6;
return maximum;
}
static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width) static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width)
{ {
float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow; float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow;

View File

@@ -17,6 +17,7 @@ namespace Slic3r
class WipeTowerWriter2; class WipeTowerWriter2;
class PrintRegionConfig; class PrintRegionConfig;
class ConfigBase;
class WipeTower2 class WipeTower2
{ {
@@ -26,7 +27,10 @@ public:
// in WipeTowerIntegration::append_tcr2 does not strip it. // in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; } static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg); static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config); static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
// Estimated total flush volume of a SEMM print with the given number of filaments,
// used to reserve wipe tower space before the tower is generated.
static float estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt);
// Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2. // Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2.

View File

@@ -3944,6 +3944,12 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
double volume = wipe_volume * filament_depth_count; double volume = wipe_volume * filament_depth_count;
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2); if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
// Sizing should take into account currently set wiping volumes.
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
const bool semm_flush = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) { if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
double depth = std::sqrt(volume / layer_height * extra_spacing); double depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || filaments_cnt > 1) { if (need_wipe_tower || filaments_cnt > 1) {
@@ -3955,30 +3961,16 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
} }
} }
else { else {
double width = m_config.prime_tower_width; double width = m_config.prime_tower_width;
if (m_config.purge_in_prime_tower && m_config.single_extruder_multi_material) { double depth = volume / (layer_height * width);
// Calculating depth should take into account currently set wiping volumes. // The flush volumes already hold the spacing between wipes.
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower) if (!semm_flush) depth *= extra_spacing;
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes. if (need_wipe_tower || depth > EPSILON) {
std::vector<std::vector<float>> wipe_volumes = WipeTower2::extract_wipe_volumes(m_config);
std::vector<float> max_wipe_volumes;
for (const std::vector<float> &v : wipe_volumes)
max_wipe_volumes.emplace_back(*std::max_element(v.begin(), v.end()));
float maximum = std::accumulate(max_wipe_volumes.begin(), max_wipe_volumes.end(), 0.f);
maximum = maximum * filaments_cnt / max_wipe_volumes.size();
// Orca: it's overshooting a bit, so let's reduce it a bit
maximum *= 0.6;
const_cast<Print *>(this)->m_wipe_tower_data.depth = maximum / (layer_height * width);
} else {
double depth = volume / (layer_height * width) * extra_spacing;
if (need_wipe_tower || m_wipe_tower_data.depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height); float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth); depth = std::max((double) min_wipe_tower_depth, depth);
} }
const_cast<Print *>(this)->m_wipe_tower_data.depth = depth; const_cast<Print *>(this)->m_wipe_tower_data.depth = depth;
} const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
const_cast<Print *>(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width;
} }
if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height); if (m_config.prime_tower_brim_width < 0) const_cast<Print *>(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height);
} }

View File

@@ -2871,6 +2871,9 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
} }
if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) {
// The tower size estimate reads printer- and filament-scope keys, which the print preset
// does not carry; built once here rather than per plate.
const DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config();
for (int plate_id = 0; plate_id < n_plates; plate_id++) { for (int plate_id = 0; plate_id < n_plates; plate_id++) {
// If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated. // If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated.
PartPlate* part_plate = ppl.get_plate(plate_id); PartPlate* part_plate = ppl.get_plate(plate_id);
@@ -2895,9 +2898,8 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
if (part_plate->get_objects_on_this_plate().empty()) continue; if (part_plate->get_objects_on_this_plate().empty()) continue;
float brim_width = print->wipe_tower_data(filaments_count).brim_width; float brim_width = print->wipe_tower_data(filaments_count).brim_width;
const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value); Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 0, false, dynamic_cast<const ConfigOptionBool*>(dconfig.option("enable_wrapping_detection"))->value);
// The stored position is already clamped onto the bed, by // The stored position is already clamped onto the bed, by
// set_default_wipe_tower_pos_for_plate and again on every drag. // set_default_wipe_tower_pos_for_plate and again on every drag.

View File

@@ -2262,6 +2262,12 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con
} }
double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1)); double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1));
if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2); if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2);
// Read from the passed plate config — m_print may not have been applied yet
// (fresh plates, CLI), in which case its PrintConfig still holds defaults.
const auto *purge_opt = config.option<ConfigOptionBool>("purge_in_prime_tower");
const auto *semm_opt = config.option<ConfigOptionBool>("single_extruder_multi_material");
const bool semm_flush = purge_opt && purge_opt->value && semm_opt && semm_opt->value;
if (semm_flush) volume = WipeTower2::estimate_semm_flush_volume(config, plate_extruder_size);
if (use_rib_wall) { if (use_rib_wall) {
depth = std::sqrt(volume / layer_height * extra_spacing); depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || plate_extruder_size > 1) { if (need_wipe_tower || plate_extruder_size > 1) {
@@ -2274,7 +2280,9 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con
} }
} }
else { else {
depth = volume/ (layer_height * w) *extra_spacing; depth = volume / (layer_height * w);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) { if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height); float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double)min_wipe_tower_depth, depth); depth = std::max((double)min_wipe_tower_depth, depth);
@@ -4380,22 +4388,21 @@ void PartPlateList::set_default_wipe_tower_pos_for_plate(int plate_idx, bool ini
f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps); f_volume_maps = wxGetApp().preset_bundle->get_default_nozzle_volume_types_for_filaments(filament_maps);
} }
DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps); DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps);
const DynamicPrintConfig &print_cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config; float w = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_tower_width"))->value;
float w = dynamic_cast<const ConfigOptionFloat *>(print_cfg.option("prime_tower_width"))->value;
float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value; float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value;
bool enable_wrapping = false; bool enable_wrapping = false;
const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection")); const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection"));
if (wrapping_opt) enable_wrapping = wrapping_opt->value; if (wrapping_opt) enable_wrapping = wrapping_opt->value;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping); Vec3d wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, init_pos ? 2 : 0, false, enable_wrapping);
if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) { if (!init_pos && (is_approx(wipe_tower_size(0), 0.0) || is_approx(wipe_tower_size(1), 0.0))) {
wipe_tower_size = part_plate->estimate_wipe_tower_size(print_cfg, w, v, nozzle_nums, 2, false, enable_wrapping); wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping);
} }
// Compute brim-aware margin: brim extends outward from tower position // Compute brim-aware margin: brim extends outward from tower position
float brim_width = 0.f; float brim_width = 0.f;
const ConfigOptionFloat *brim_opt = print_cfg.option<ConfigOptionFloat>("prime_tower_brim_width"); const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width");
if (brim_opt) { if (brim_opt) {
brim_width = brim_opt->value; brim_width = brim_opt->value;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z()); if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());