Extract and Unify Wipe Tower Estimation

This commit is contained in:
Hanif Koh
2026-09-04 17:15:19 +08:00
committed by HanifKoh
parent 8a291f9d56
commit 8df5e5e738
13 changed files with 589 additions and 227 deletions

View File

@@ -4015,7 +4015,7 @@ int CLI::run(int argc, char **argv)
} }
}; };
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, new_extruder_count](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) { auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box(); plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}") BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z(); %(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
@@ -4059,22 +4059,13 @@ int CLI::run(int argc, char **argv)
plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index); plate_obj_size_info.wipe_x = wipe_x_option->get_at(plate_index);
plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index); plate_obj_size_info.wipe_y = wipe_y_option->get_at(plate_index);
ConfigOptionFloat* width_option = print_config.option<ConfigOptionFloat>("prime_tower_width", true); // Body and brim from one estimate: resolving an auto (-1) brim against a different
plate_obj_size_info.wipe_width = width_option->value; // height would size the two halves of the same tower from two different objects.
const WipeTowerFootprint footprint = plate->estimate_wipe_tower_footprint(print_config, filaments_cnt);
float brim_width = float(footprint.brim_width);
ConfigOptionFloat* brim_width_option = print_config.option<ConfigOptionFloat>("prime_tower_brim_width", true); plate_obj_size_info.wipe_width = footprint.width;
float brim_width = brim_width_option->value; plate_obj_size_info.wipe_depth = footprint.depth;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float)plate_obj_size_info.obj_bbox.max.z());
ConfigOptionFloat* volume_option = print_config.option<ConfigOptionFloat>("prime_volume", true);
float wipe_volume = volume_option->value;
const ConfigOptionBool * wrapping_detection = print_config.option<ConfigOptionBool>("enable_wrapping_detection");
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value;
Vec3d wipe_tower_size = plate->estimate_wipe_tower_size(print_config, plate_obj_size_info.wipe_width, wipe_volume, new_extruder_count, filaments_cnt, false, enable_wrapping);
plate_obj_size_info.wipe_width = wipe_tower_size(0);
plate_obj_size_info.wipe_depth = wipe_tower_size(1);
Vec3d origin = plate->get_origin(); Vec3d origin = plate->get_origin();
Vec3d start(origin(0) + plate_obj_size_info.wipe_x - brim_width, origin(1) + plate_obj_size_info.wipe_y, 0.f); Vec3d start(origin(0) + plate_obj_size_info.wipe_x - brim_width, origin(1) + plate_obj_size_info.wipe_y, 0.f);
@@ -4875,7 +4866,7 @@ int CLI::run(int argc, char **argv)
wipe_y_option->set_at(&wt_y_opt, i, 0); wipe_y_option->set_at(&wt_y_opt, i, 0);
Vec3d wipe_tower_size, wipe_tower_pos; Vec3d wipe_tower_size, wipe_tower_pos;
ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, new_extruder_count, assemble_plate.filaments_count, true); ArrangePolygon wipe_tower_ap = cur_plate->estimate_wipe_tower_polygon(m_print_config, i, wipe_tower_pos, wipe_tower_size, assemble_plate.filaments_count, true);
//update the new wp position //update the new wp position
wt_x_opt.value = wipe_tower_pos(0); wt_x_opt.value = wipe_tower_pos(0);
@@ -5175,7 +5166,7 @@ int CLI::run(int argc, char **argv)
} }
Vec3d wipe_tower_size, wipe_tower_pos; Vec3d wipe_tower_size, wipe_tower_pos;
ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, new_extruder_count, extruder_size, true); ArrangePolygon wipe_tower_ap = partplate_list.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(m_print_config, plate_index_valid, wipe_tower_pos, wipe_tower_size, extruder_size, true);
//update the new wp position //update the new wp position
if (bedid < plate_count) { if (bedid < plate_count) {
@@ -5276,22 +5267,16 @@ int CLI::run(int argc, char **argv)
//float depth = v * (filaments_cnt - 1) / (layer_height * w); //float depth = v * (filaments_cnt - 1) / (layer_height * w);
const ConfigOptionBool *wrapping_detection = m_print_config.option<ConfigOptionBool>("enable_wrapping_detection"); const WipeTowerFootprint footprint = cur_plate->estimate_wipe_tower_footprint(m_print_config, filaments_cnt);
bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value; Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping);
Vec3d plate_origin = cur_plate->get_origin(); Vec3d plate_origin = cur_plate->get_origin();
int plate_width, plate_depth; int plate_width, plate_depth;
double plate_height; double plate_height;
partplate_list.get_plate_size(plate_width, plate_depth, plate_height); partplate_list.get_plate_size(plate_width, plate_depth, plate_height);
float depth = wipe_tower_size(1); float depth = wipe_tower_size(1);
float margin = 15.f, wp_brim_width = 0.f; // Brim already resolved against the height the body was sized from.
ConfigOption *wipe_tower_brim_width_opt = m_print_config.option("prime_tower_brim_width"); float margin = 15.f, wp_brim_width = float(footprint.brim_width);
if (wipe_tower_brim_width_opt ) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
wp_brim_width = wipe_tower_brim_width_opt->getFloat();
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width;
}
w = wipe_tower_size(0); w = wipe_tower_size(0);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: x=%1%, y=%2%, width=%3%, depth=%4%, angle=%5%, prime_volume=%6%, filaments_cnt=%7%, layer_height=%8%, plate_width=%9%, plate_depth=%10%") BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: x=%1%, y=%2%, width=%3%, depth=%4%, angle=%5%, prime_volume=%6%, filaments_cnt=%7%, layer_height=%8%, plate_width=%9%, plate_depth=%10%")

View File

@@ -272,6 +272,8 @@ set(lisbslic3r_sources
GCode/WipeTower2.hpp GCode/WipeTower2.hpp
GCode/WipeTower.cpp GCode/WipeTower.cpp
GCode/WipeTower.hpp GCode/WipeTower.hpp
GCode/WipeTowerEstimate.cpp
GCode/WipeTowerEstimate.hpp
GCodeWriter.cpp GCodeWriter.cpp
GCodeWriter.hpp GCodeWriter.hpp
Geometry/ArcWelder.hpp Geometry/ArcWelder.hpp

View File

@@ -0,0 +1,109 @@
#include "WipeTowerEstimate.hpp"
#include "WipeTower.hpp"
#include "WipeTower2.hpp"
#include "../Config.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
#include <algorithm>
#include <cmath>
namespace Slic3r {
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, size_t filaments_cnt, double layer_height, double max_object_height, bool any_raft)
{
WipeTowerFootprint footprint;
footprint.height = max_object_height;
if (filaments_cnt == 0 || layer_height < EPSILON)
return footprint;
// Every caller today declares all these keys, but the signature accepts any ConfigBase:
// fall back to the key's declared default, never to a hand-copied constant.
auto option_of = [&config](const char *key) -> const ConfigOption * {
if (const ConfigOption *opt = config.option(key); opt != nullptr)
return opt;
if (const ConfigDef *def = config.def(); def != nullptr)
if (const ConfigOptionDef *opt_def = def->get(key); opt_def != nullptr)
return opt_def->default_value.get();
return nullptr;
};
auto opt_float = [&option_of](const char *key) {
const ConfigOption *opt = option_of(key);
return opt != nullptr ? opt->getFloat() : 0.;
};
auto opt_bool = [&option_of](const char *key) {
const ConfigOption *opt = option_of(key);
return opt != nullptr && opt->getBool();
};
// By value, not by concrete type: a static PrintConfig holds ConfigOptionEnum<T>, a
// DynamicConfig built from presets holds ConfigOptionEnumGeneric, and both answer getInt().
auto opt_enum = [&option_of](const char *key, int fallback) {
const ConfigOption *opt = option_of(key);
return opt != nullptr ? opt->getInt() : fallback;
};
auto max_of = [&option_of](const char *key, double fallback) {
const auto *opt = dynamic_cast<const ConfigOptionFloats *>(option_of(key));
return (opt != nullptr && !opt->values.empty()) ? *std::max_element(opt->values.begin(), opt->values.end()) : fallback;
};
const double width = opt_float("prime_tower_width");
const double prime_volume = opt_float("prime_volume");
const double extra_spacing = opt_float("prime_tower_infill_gap") / 100.;
double rib_width = opt_float("wipe_tower_rib_width");
const double extra_rib_length = opt_float("wipe_tower_extra_rib_length");
const auto *nozzle_opt = dynamic_cast<const ConfigOptionFloats *>(option_of("nozzle_diameter"));
const bool dual_nozzle = nozzle_opt != nullptr && nozzle_opt->values.size() == 2;
const bool rib_wall = opt_enum("wipe_tower_wall_type", int(WipeTowerWallType::wtwRectangle)) == int(WipeTowerWallType::wtwRib);
const bool smooth_timelapse = opt_enum("timelapse_type", int(TimelapseType::tlTraditional)) == int(TimelapseType::tlSmooth);
// Reasons a tower is printed with no tool change to purge for.
const bool need_wipe_tower = smooth_timelapse || opt_bool("enable_wrapping_detection") || any_raft;
// No tool change, nothing to purge; smooth timelapse still primes once.
size_t purge_count = 0;
if (filaments_cnt > 1)
purge_count = dual_nozzle ? filaments_cnt : filaments_cnt - 1;
else if (smooth_timelapse)
purge_count = 1;
double volume = prime_volume * double(purge_count);
if (dual_nozzle) {
// Dual-nozzle printers also purge the filament change length on the tower.
const double length = max_of("filament_change_length", 0.);
const double diameter = max_of("filament_diameter", 1.75);
volume += length * PI * diameter * diameter / 4. * double(filaments_cnt / 2);
}
// Single-extruder multi-material purges the flush matrix instead of the prime volume.
const bool semm_flush = opt_bool("purge_in_prime_tower") && opt_bool("single_extruder_multi_material");
if (semm_flush)
volume = WipeTower2::estimate_semm_flush_volume(config, filaments_cnt);
// Both wall types decide this together: over-reserving only wastes bed area, but
// reporting no tower for one that is built collapses the validation hull to a point.
if (volume < EPSILON && !need_wipe_tower)
return footprint;
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
if (rib_wall) {
// A rib wall squares the tower; the ribs run the diagonal and bulge past the body.
const double volume_depth = std::sqrt(volume / layer_height * extra_spacing);
double depth = std::max(min_depth, volume_depth);
rib_width = std::min(rib_width, depth / 2.);
depth = rib_width / std::sqrt(2.) + std::max(depth + extra_rib_length, volume_depth);
footprint.width = footprint.depth = depth;
} else {
double depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush)
depth *= extra_spacing;
footprint.width = width;
footprint.depth = std::max(min_depth, depth);
}
footprint.brim_width = opt_float("prime_tower_brim_width");
if (footprint.brim_width < 0)
footprint.brim_width = WipeTower::get_auto_brim_by_height(float(max_object_height));
return footprint;
}
} // namespace Slic3r

View File

@@ -0,0 +1,29 @@
#pragma once
#include <cstddef>
namespace Slic3r {
class ConfigBase;
// Pre-slice footprint of the wipe tower, shared by validation (Print), the GUI's placement
// clamp/preview/arrange and the CLI placement. The arithmetic is shared; the inputs below are
// not, so a change to how one caller derives them has to be mirrored in the others.
struct WipeTowerFootprint
{
double width = 0.; // effective width: equals depth for a rib wall, which squares the tower
double depth = 0.; // 0 when these inputs imply no tower
double height = 0.; // tallest object; drives the stability floor and the auto brim
double brim_width = 0.; // configured width, auto (-1) resolved by height
};
// filaments_cnt: filaments purged on the plate. The config cannot see custom G-code tool
// changes, so a count derived from the model must include them
// (Print::extruders(true)) or a real tower is sized as if it were never built.
// layer_height: thinnest layer the tower will be planned at.
// any_raft: any object on the plate prints a raft, which puts the tower on every layer
// below it. Caller-resolved: raft_layers is a PrintObjectConfig key, absent
// from Print's config and overridable per object.
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, size_t filaments_cnt, double layer_height, double max_object_height, bool any_raft);
} // namespace Slic3r

View File

@@ -20,6 +20,7 @@
#include "GCode.hpp" #include "GCode.hpp"
#include "GCode/WipeTower.hpp" #include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp" #include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
#include "Utils.hpp" #include "Utils.hpp"
#include "PrintConfig.hpp" #include "PrintConfig.hpp"
#include "MaterialType.hpp" #include "MaterialType.hpp"
@@ -1031,20 +1032,20 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
//BBS: add the wipe tower check logic //BBS: add the wipe tower check logic
const PrintConfig & config = print.config(); const PrintConfig & config = print.config();
int filaments_count = print.extruders().size(); // Custom G-code tool changes (MultiAsSingle) build a real tower on a plate whose objects
// all use one filament, so they have to be counted or the hull below collapses to a point.
int filaments_count = print.extruders(true).size();
int plate_index = print.get_plate_index(); int plate_index = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin(); const Vec3d plate_origin = print.get_plate_origin();
float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0); float x = config.wipe_tower_x.get_at(plate_index) + plate_origin(0);
float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1); float y = config.wipe_tower_y.get_at(plate_index) + plate_origin(1);
float width = config.prime_tower_width.value;
float a = config.wipe_tower_rotation_angle.value; float a = config.wipe_tower_rotation_angle.value;
//float v = config.wiping_volume.value; //float v = config.wiping_volume.value;
float depth = print.wipe_tower_data(filaments_count).depth; // The estimate resolves the effective width (a rib wall squares the tower).
//float brim_width = print.wipe_tower_data(filaments_count).brim_width; const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count);
float width = wipe_tower_estimate.width;
if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) float depth = wipe_tower_estimate.depth;
width = depth;
Polygons convex_hulls_temp; Polygons convex_hulls_temp;
if (print.has_wipe_tower()) { if (print.has_wipe_tower()) {
@@ -3997,74 +3998,27 @@ bool Print::has_wipe_tower() const
const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
{ {
// If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default. // Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so
double max_height = 0; // validation cannot reject a position the clamp just accepted.
for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) { if (is_step_done(psWipeTower) || filaments_cnt == 0)
double object_z = (double) m_objects[obj_idx]->size().z(); return m_wipe_tower_data;
max_height = std::max(unscale_(object_z), max_height);
double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
bool any_raft = false;
for (const PrintObject *object : m_objects) {
max_height = std::max(max_height, unscale_(double(object->size().z())));
layer_height = std::min(layer_height, object->config().layer_height.value);
any_raft = any_raft || object->config().raft_layers.value > 0;
} }
if (max_height < EPSILON) return m_wipe_tower_data; if (max_height < EPSILON)
return m_wipe_tower_data;
double layer_height = 0.08f; // hard code layer height const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, filaments_cnt, layer_height, max_height, any_raft);
layer_height = m_objects.front()->config().layer_height.value; WipeTowerData &data = const_cast<Print *>(this)->m_wipe_tower_data;
data.depth = float(footprint.depth);
auto timelapse_type = config().option<ConfigOptionEnum<TimelapseType>>("timelapse_type"); data.width = float(footprint.width);
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib); data.brim_width = float(footprint.brim_width);
double extra_spacing = config().option("prime_tower_infill_gap")->getFloat() / 100.;
double rib_width = config().option("wipe_tower_rib_width")->getFloat();
double filament_change_volume = 0.;
{
std::vector<double> filament_change_lengths;
auto filament_change_lengths_opt = config().option<ConfigOptionFloats>("filament_change_length");
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
double diameter = 1.75;
std::vector<double> diameters;
auto filament_diameter_opt = config().option<ConfigOptionFloats>("filament_diameter");
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
filament_change_volume = length * PI * diameter * diameter / 4.;
}
if (! is_step_done(psWipeTower) && filaments_cnt !=0) {
double wipe_volume = m_config.prime_volume;
int filament_depth_count = m_config.nozzle_diameter.values.size() == 2 ? filaments_cnt : filaments_cnt - 1;
if (filaments_cnt == 1 && enable_timelapse_print()) filament_depth_count = 1;
double volume = wipe_volume * filament_depth_count;
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) {
double depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || filaments_cnt > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double) min_wipe_tower_depth, depth);
depth += rib_width / std::sqrt(2) + config().wipe_tower_extra_rib_length.value;
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;
}
}
else {
double width = m_config.prime_tower_width;
double depth = volume / (layer_height * width);
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
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.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);
}
return m_wipe_tower_data; return m_wipe_tower_data;
} }

View File

@@ -782,6 +782,9 @@ struct WipeTowerData
// Depth of the wipe tower to pass to GLCanvas3D for exact bounding box: // Depth of the wipe tower to pass to GLCanvas3D for exact bounding box:
float depth; float depth;
// Effective width (a rib wall squares the tower). Pre-generation estimate only; once the
// tower exists its mesh is exact.
float width;
std::vector<std::pair<float, float>> z_and_depth_pairs; std::vector<std::pair<float, float>> z_and_depth_pairs;
float brim_width; float brim_width;
float height; float height;
@@ -795,6 +798,7 @@ struct WipeTowerData
used_filament.clear(); used_filament.clear();
number_of_toolchanges = -1; number_of_toolchanges = -1;
depth = 0.f; depth = 0.f;
width = 0.f;
brim_width = 0.f; brim_width = 0.f;
height = 0.f; height = 0.f;
rib_offset = Vec2f::Zero(); rib_offset = Vec2f::Zero();

View File

@@ -2891,20 +2891,18 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config;
float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id); float x = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_x"))->get_at(plate_id);
float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id); float y = dynamic_cast<const ConfigOptionFloats*>(proj_cfg.option("wipe_tower_y"))->get_at(plate_id);
float w = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_tower_width"))->value;
float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value; float a = dynamic_cast<const ConfigOptionFloat*>(m_config->option("wipe_tower_rotation_angle"))->value;
// BBS
float v = dynamic_cast<const ConfigOptionFloat*>(m_config->option("prime_volume"))->value;
Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin();
const Print* print = m_process->fff_print();
const Print* current_print = part_plate->fff_print(); const Print* current_print = part_plate->fff_print();
if (!need_wipe_tower && part_plate->get_extruders(true).size() < 2) continue; if (!need_wipe_tower && part_plate->get_extruders(true).size() < 2) continue;
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; // Body and brim from this plate's own estimate: m_process->fff_print() is the
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); // selected plate's, so an auto brim drew every tower with that plate's brim.
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); const WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config);
float brim_width = float(footprint.brim_width);
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
// 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

@@ -265,8 +265,7 @@ arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<i
int extruder_size = extruder_ids.size(); int extruder_size = extruder_ids.size();
Vec3d wipe_tower_size, wipe_tower_pos; Vec3d wipe_tower_size, wipe_tower_pos;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); auto arrange_poly = ppl.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(full_config, plate_index, wipe_tower_pos, wipe_tower_size, extruder_size);
auto arrange_poly = ppl.get_plate(plate_index_valid)->estimate_wipe_tower_polygon(full_config, plate_index, wipe_tower_pos, wipe_tower_size, nozzle_nums, extruder_size);
arrange_poly.bed_idx = plate_index; arrange_poly.bed_idx = plate_index;
return arrange_poly; return arrange_poly;
} }

View File

@@ -20,6 +20,7 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/BoundingBox.hpp" #include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
@@ -1531,8 +1532,15 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
if (check_objects_empty_and_gcode3mf(plate_extruders)) { if (check_objects_empty_and_gcode3mf(plate_extruders)) {
return plate_extruders; return plate_extruders;
} }
// if 3mf file return get_extruders(conside_custom_gcode, wxGetApp().preset_bundle->prints.get_edited_preset().config, wxGetApp().preset_bundle->project_config);
const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; }
// The plate's filaments, with the global keys read from the given configs rather than the
// application's presets: the wipe tower estimate is also called under the CLI, which has no
// application object. get_extruders(bool) passes the edited presets; a full config serves both.
std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const
{
std::vector<int> plate_extruders;
int glb_support_intf_extr = glb_config.opt_int("support_interface_filament"); int glb_support_intf_extr = glb_config.opt_int("support_interface_filament");
int glb_support_extr = glb_config.opt_int("support_filament"); int glb_support_extr = glb_config.opt_int("support_filament");
int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id"); int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id");
@@ -1549,7 +1557,9 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
glb_support |= glb_config.opt_int("raft_layers") > 0; glb_support |= glb_config.opt_int("raft_layers") > 0;
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) {
if (!contain_instance_totally(obj_idx, 0)) // Any instance on the plate counts, as PrintApply does: after an arrange, instance 0
// can sit on a different plate.
if (!contain_any_instance_totally(obj_idx))
continue; continue;
ModelObject* mo = m_model->objects[obj_idx]; ModelObject* mo = m_model->objects[obj_idx];
@@ -1662,7 +1672,7 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
if (conside_custom_gcode) { if (conside_custom_gcode) {
//BBS //BBS
int nums_extruders = 0; int nums_extruders = 0;
if (const ConfigOptionStrings *color_option = dynamic_cast<const ConfigOptionStrings *>(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { if (const ConfigOptionStrings *color_option = dynamic_cast<const ConfigOptionStrings *>(project_config.option("filament_colour"))) {
nums_extruders = color_option->values.size(); nums_extruders = color_option->values.size();
if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) { if (m_model->plates_custom_gcodes.find(m_plate_index) != m_model->plates_custom_gcodes.end()) {
for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) { for (auto item : m_model->plates_custom_gcodes.at(m_plate_index).gcodes) {
@@ -1681,9 +1691,8 @@ std::vector<int> PartPlate::get_extruders(bool conside_custom_gcode) const
// is never loaded into a tray, so callers (AMS mapping, filament checks) must see the // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the
// physical filaments it resolves to instead. // physical filaments it resolves to instead.
{ {
auto& project_config = wxGetApp().preset_bundle->project_config; const auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
auto* is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed"); const auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
auto* comp_strs_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) {
std::vector<unsigned int> ext_0based; std::vector<unsigned int> ext_0based;
for (int e : plate_extruders) for (int e : plate_extruders)
@@ -2311,113 +2320,86 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig
return wipe_tower_size; return wipe_tower_size;
}*/ }*/
Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, const double w, const double wipe_volume, int extruder_count, int plate_extruder_size, bool use_global_objects, bool enable_wrapping_detection) const WipeTowerFootprint PartPlate::estimate_wipe_tower_footprint(const DynamicPrintConfig &config, int plate_extruder_size, bool use_global_objects) const
{ {
Vec3d wipe_tower_size; // The CLI calls this too, so the plate's filaments are derived from the passed config:
double layer_height = 0.08f; // hard code layer height // get_extruders(bool) reads the same keys off wxGetApp()'s presets, which the CLI has none of.
double max_height = 0.f; std::vector<int> plate_extruders;
wipe_tower_size.setZero(); if (plate_extruder_size == 0) {
plate_extruders = get_extruders(true, config, config);
const ConfigOption* layer_height_opt = config.option("layer_height"); plate_extruder_size = int(plate_extruders.size());
if (layer_height_opt) }
layer_height = layer_height_opt->getFloat(); // The wipe tower filament joins the tool ordering even when unused (Print::extruders), so
// validation counts it. An explicit count is the plate's painted filaments, which never do.
// empty plate const ConfigOption *wipe_tower_filament_opt = config.option("wipe_tower_filament");
if (plate_extruder_size == 0) const int wipe_tower_filament = wipe_tower_filament_opt != nullptr ? wipe_tower_filament_opt->getInt() : 0;
{ if (plate_extruder_size > 1 && wipe_tower_filament > 0) {
std::vector<int> plate_extruders = get_extruders(true); if (plate_extruders.empty())
plate_extruder_size = plate_extruders.size(); plate_extruders = get_extruders(true, config, config);
if (std::find(plate_extruders.begin(), plate_extruders.end(), wipe_tower_filament) == plate_extruders.end())
++plate_extruder_size;
} }
if (plate_extruder_size == 0) if (plate_extruder_size == 0)
return wipe_tower_size; return WipeTowerFootprint();
for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { // Tallest object on this plate and the thinnest layer it is sliced at, resolved per object
if (!use_global_objects && !contain_instance_totally(obj_idx, 0)) // as PrintObject resolves them (override, else preset) and over this plate's objects only -
// seeding from the global value, or folding in an off-plate override, diverges from Print.
const ConfigOption *layer_height_opt = config.option("layer_height");
const double global_layer_height = layer_height_opt != nullptr ? layer_height_opt->getFloat() : 0.08;
const ConfigOption *raft_layers_opt = config.option("raft_layers");
const int global_raft_layers = raft_layers_opt != nullptr ? raft_layers_opt->getInt() : 0;
double max_height = 0.;
double layer_height = std::numeric_limits<double>::max();
bool any_raft = false;
for (int obj_idx = 0; obj_idx < int(m_model->objects.size()); ++obj_idx) {
const ModelObject *object = m_model->objects[obj_idx];
if (!use_global_objects && !contain_any_instance_totally(obj_idx))
continue; continue;
// Per instance, to match PrintObject::size(); the union over instances differs once
BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box_exact(); // they are rotated apart.
max_height = std::max(bbox.size().z(), max_height); for (int inst_idx = 0; inst_idx < int(object->instances.size()); ++inst_idx) {
} if (!use_global_objects && !contain_instance_totally(obj_idx, inst_idx))
wipe_tower_size(2) = max_height; continue;
//const DynamicPrintConfig &dconfig = wxGetApp().preset_bundle->prints.get_edited_preset().config; max_height = std::max(max_height, object->instance_bounding_box(inst_idx, true).size().z());
auto timelapse_type = config.option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | enable_wrapping_detection;
double extra_spacing = config.option("prime_tower_infill_gap")->getFloat() / 100.;
const ConfigOptionEnum<WipeTowerWallType>* use_rib_wall_opt = config.option<ConfigOptionEnum<WipeTowerWallType>>("wipe_tower_wall_type");
bool use_rib_wall = use_rib_wall_opt ? use_rib_wall_opt->value == WipeTowerWallType::wtwRib: false;
double rib_width = config.option("wipe_tower_rib_width")->getFloat();
double depth;
double filament_change_volume=0.;
{
std::vector<double> filament_change_lengths;
auto filament_change_lengths_opt = m_print->config().option<ConfigOptionFloats>("filament_change_length");
if (filament_change_lengths_opt) filament_change_lengths = filament_change_lengths_opt->values;
double length = filament_change_lengths.empty() ? 0 : *std::max_element(filament_change_lengths.begin(), filament_change_lengths.end());
double diameter = 1.75;
std::vector<double> diameters;
auto filament_diameter_opt = m_print->config().option<ConfigOptionFloats>("filament_diameter");
if (filament_diameter_opt) diameters = filament_diameter_opt->values;
diameter = diameters.empty() ? diameter : *std::max_element(diameters.begin(), diameters.end());
filament_change_volume = length * PI * diameter * diameter / 4.;
}
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);
// 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) {
depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || plate_extruder_size > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
double volume_depth = depth;
depth = std::max((double) min_wipe_tower_depth, depth);
rib_width = std::min(rib_width, depth / 2);
depth = rib_width / std::sqrt(2) + std::max(depth + m_print->config().wipe_tower_extra_rib_length.value, volume_depth);
wipe_tower_size(0) = wipe_tower_size(1) = depth;
} }
const ConfigOption *object_layer_height = object->config.option("layer_height");
layer_height = std::min(layer_height, object_layer_height != nullptr ? object_layer_height->getFloat() : global_layer_height);
const ConfigOption *object_raft_layers = object->config.option("raft_layers");
any_raft = any_raft || (object_raft_layers != nullptr ? object_raft_layers->getInt() : global_raft_layers) > 0;
} }
else { if (layer_height == std::numeric_limits<double>::max())
depth = volume / (layer_height * w); layer_height = global_layer_height;
// The flush volumes already hold the spacing between wipes.
if (!semm_flush) depth *= extra_spacing;
if (need_wipe_tower || depth > EPSILON) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
depth = std::max((double)min_wipe_tower_depth, depth);
}
wipe_tower_size(0) = w;
wipe_tower_size(1) = depth;
}
return wipe_tower_size; return Slic3r::estimate_wipe_tower_footprint(config, size_t(plate_extruder_size), layer_height, max_height, any_raft);
} }
arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const DynamicPrintConfig& config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int extruder_count, int plate_extruder_size, bool use_global_objects) const Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig &config, int plate_extruder_size, bool use_global_objects) const
{
const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(config, plate_extruder_size, use_global_objects);
return Vec3d(footprint.width, footprint.depth, footprint.height);
}
arrangement::ArrangePolygon PartPlate::estimate_wipe_tower_polygon(const DynamicPrintConfig& config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int plate_extruder_size, bool use_global_objects) const
{ {
float x = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_x"))->get_at(plate_index); float x = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_x"))->get_at(plate_index);
float y = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_y"))->get_at(plate_index); float y = dynamic_cast<const ConfigOptionFloats*>(config.option("wipe_tower_y"))->get_at(plate_index);
float w = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_tower_width"))->value;
//float a = dynamic_cast<const ConfigOptionFloat*>(config.option("wipe_tower_rotation_angle"))->value; //float a = dynamic_cast<const ConfigOptionFloat*>(config.option("wipe_tower_rotation_angle"))->value;
float v = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_volume"))->value; const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(config, plate_extruder_size, use_global_objects);
float tower_brim_width = dynamic_cast<const ConfigOptionFloat*>(config.option("prime_tower_brim_width"))->value; wt_size = Vec3d(footprint.width, footprint.depth, footprint.height);
const ConfigOptionBool * wrapping_opt = dynamic_cast<const ConfigOptionBool *>(config.option("enable_wrapping_detection"));
bool enable_wrapping = (wrapping_opt != nullptr) && wrapping_opt->value;
wt_size = estimate_wipe_tower_size(config, w, v, extruder_count, plate_extruder_size, use_global_objects, enable_wrapping);
int plate_width=m_width, plate_depth=m_depth; int plate_width=m_width, plate_depth=m_depth;
w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower float w = wt_size(0); // effective width; differs from prime_tower_width when the rib wall squares the tower
float depth = wt_size(1); float depth = wt_size(1);
float margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f; // Resolved brim, not the raw option: "Auto" (-1) would yield a margin of 0 and let the
const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width"); // clamp put the brim off the bed. Matches set_default_wipe_tower_pos_for_plate.
if (wipe_tower_brim_width_opt) { const float wp_brim_width = float(footprint.brim_width);
wp_brim_width = wipe_tower_brim_width_opt->getFloat(); const float margin = WIPE_TOWER_MARGIN + wp_brim_width;
if (wp_brim_width < 0) wp_brim_width = WipeTower::get_auto_brim_by_height((float) wt_size.z()); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%") % wp_brim_width;
}
x = std::clamp(x, margin, (float)plate_width - w - margin - wp_brim_width); // A tower too deep for the plate leaves no valid position: clamping with hi < lo is UB and
y = std::clamp(y, margin, (float)plate_depth - depth - margin - wp_brim_width); // in release silently returns the negative hi.
x = std::clamp(x, margin, std::max(margin, (float)plate_width - w - margin));
y = std::clamp(y, margin, std::max(margin, (float)plate_depth - depth - margin));
wt_pos(0) = x; wt_pos(0) = x;
wt_pos(1) = y; wt_pos(1) = y;
wt_pos(2) = 0.f; wt_pos(2) = 0.f;
@@ -2755,6 +2737,20 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const
return result; return result;
} }
//judge whether any of the object's instances is totally included in plate or not
bool PartPlate::contain_any_instance_totally(int obj_id) const
{
if (obj_id < 0 || obj_id >= int(m_model->objects.size()))
return false;
const ModelObject *object = m_model->objects[obj_id];
for (int instance_id = 0; instance_id < int(object->instances.size()); ++instance_id)
if (contain_instance_totally(obj_id, instance_id))
return true;
return false;
}
//check whether instance is outside the plate or not //check whether instance is outside the plate or not
bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box) bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* bounding_box)
{ {
@@ -4488,26 +4484,16 @@ 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);
float w = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_tower_width"))->value; WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config, init_pos ? 2 : 0);
float v = dynamic_cast<const ConfigOptionFloat *>(full_config.option("prime_volume"))->value;
bool enable_wrapping = false;
const ConfigOptionBool *wrapping_opt = dynamic_cast<const ConfigOptionBool *>(full_config.option("enable_wrapping_detection"));
if (wrapping_opt) enable_wrapping = wrapping_opt->value;
int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
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(footprint.width, 0.0) || is_approx(footprint.depth, 0.0))) {
wipe_tower_size = part_plate->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 2, false, enable_wrapping); footprint = part_plate->estimate_wipe_tower_footprint(full_config, 2);
} }
Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height);
// Compute brim-aware margin: brim extends outward from tower position // Brim-aware margin: the brim extends outward from the tower position.
float brim_width = 0.f; const float brim_width = float(footprint.brim_width);
const ConfigOptionFloat *brim_opt = full_config.option<ConfigOptionFloat>("prime_tower_brim_width"); const float margin = WIPE_TOWER_MARGIN + brim_width;
if (brim_opt) {
brim_width = brim_opt->value;
if (brim_width < 0) brim_width = WipeTower::get_auto_brim_by_height((float) wipe_tower_size.z());
}
const float margin = WIPE_TOWER_MARGIN + brim_width;
// clamp wipe tower position within plate boundaries // clamp wipe tower position within plate boundaries
{ {

View File

@@ -11,6 +11,7 @@
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/Slicing.hpp" #include "libslic3r/Slicing.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/Arrange.hpp" #include "libslic3r/Arrange.hpp"
#include "Plater.hpp" #include "Plater.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -339,11 +340,14 @@ public:
Vec3d get_origin() { return m_origin; } Vec3d get_origin() { return m_origin; }
//Vec3d calculate_wipe_tower_size(const DynamicPrintConfig &config, const double w, const double wipe_volume, int plate_extruder_size = 0, bool use_global_objects = false) const; //Vec3d calculate_wipe_tower_size(const DynamicPrintConfig &config, const double w, const double wipe_volume, int plate_extruder_size = 0, bool use_global_objects = false) const;
Vec3d estimate_wipe_tower_size(const DynamicPrintConfig & config, const double w, const double wipe_volume, int extruder_count = 1, int plate_extruder_size = 0, bool use_global_objects = false, bool enable_wrapping_detection = false) const; // plate_extruder_size: filaments purged on the plate; 0 derives it from the plate's objects.
arrangement::ArrangePolygon estimate_wipe_tower_polygon(const DynamicPrintConfig & config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int extruder_count = 1, int plate_extruder_size = 0, bool use_global_objects = false) const; WipeTowerFootprint estimate_wipe_tower_footprint(const DynamicPrintConfig & config, int plate_extruder_size = 0, bool use_global_objects = false) const;
Vec3d estimate_wipe_tower_size(const DynamicPrintConfig & config, int plate_extruder_size = 0, bool use_global_objects = false) const;
arrangement::ArrangePolygon estimate_wipe_tower_polygon(const DynamicPrintConfig & config, int plate_index, Vec3d& wt_pos, Vec3d& wt_size, int plate_extruder_size = 0, bool use_global_objects = false) const;
bool check_objects_empty_and_gcode3mf(std::vector<int> &result) const; bool check_objects_empty_and_gcode3mf(std::vector<int> &result) const;
// get used filaments from config, 1 based idx // get used filaments from config, 1 based idx
std::vector<int> get_extruders(bool conside_custom_gcode = false) const; std::vector<int> get_extruders(bool conside_custom_gcode = false) const;
std::vector<int> get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const;
std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const; std::vector<int> get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const;
std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const; std::vector<int> get_extruders_without_support(bool conside_custom_gcode = false) const;
// get used filaments from gcode result, 1 based idx // get used filaments from gcode result, 1 based idx
@@ -366,6 +370,8 @@ public:
bool contain_instance_totally(ModelObject* object, int instance_id) const; bool contain_instance_totally(ModelObject* object, int instance_id) const;
//judge whether instance is totally included in plate or not //judge whether instance is totally included in plate or not
bool contain_instance_totally(int obj_id, int instance_id) const; bool contain_instance_totally(int obj_id, int instance_id) const;
//judge whether any of the object's instances is totally included in plate or not
bool contain_any_instance_totally(int obj_id) const;
//judge whether the plate's origin is at the left of instance or not //judge whether the plate's origin is at the left of instance or not
bool is_left_top_of(int obj_id, int instance_id); bool is_left_top_of(int obj_id, int instance_id);

View File

@@ -182,3 +182,86 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected)); CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected));
} }
} }
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
// they call the estimator directly.
static DynamicPrintConfig tower_estimate_config(const char *wall_type)
{
// 100 mm3 per purge on a 50 mm wide tower: one purge is 100/(layer_height * 50) of depth.
return multifilament_config(2, {
{ "enable_prime_tower", "1" },
{ "wipe_tower_wall_type", wall_type },
{ "prime_tower_width", "50" },
{ "prime_volume", "100" },
{ "prime_tower_infill_gap", "100%" },
{ "prime_tower_brim_width", "3" },
{ "purge_in_prime_tower", "0" },
{ "single_extruder_multi_material", "0" },
{ "timelapse_type", "0" },
{ "layer_height", "0.2" },
{ "raft_layers", "0" } });
}
TEST_CASE("The tower is sized for the thinnest layer any object on the plate is sliced at", "[WipeTower]")
{
// The tower has to survive its thinnest layer, so an override finer than the preset drives
// the estimate even on the second object. Two 20 mm cubes, the second at 0.1 mm.
const DynamicPrintConfig config = tower_estimate_config("rectangle");
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides = {
{}, { { "layer_height", "0.1" } } };
Print print;
Model model;
init_print({ cube(20), cube(20) }, print, model, config, &overrides);
// One purge at 0.1 mm: 100 / (0.1 * 50) = 20 mm, above the 20 mm-tall tower's stability
// floor. At the preset's 0.2 mm it would be half that, so the two are easy to tell apart.
const float floor_20mm = WipeTower::get_limit_depth_by_height(20.f);
REQUIRE(floor_20mm < 10.f);
CHECK_THAT(print.wipe_tower_data(2).depth, Catch::Matchers::WithinAbs(20., 1e-4));
}
TEST_CASE("Validation is given the tower's effective width, not the configured one", "[WipeTower]")
{
// A rib wall squares the tower, so its width is its depth. Validation reads this rather
// than re-deriving the rule from the wall type.
Print print;
Model model;
SECTION("a rectangle wall keeps the configured width") {
const DynamicPrintConfig config = tower_estimate_config("rectangle");
init_print({ cube(20) }, print, model, config);
const WipeTowerData &data = print.wipe_tower_data(2);
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(50., 1e-4));
CHECK(data.depth < data.width);
}
SECTION("a rib wall reports the squared footprint") {
const DynamicPrintConfig config = tower_estimate_config("rib");
init_print({ cube(20) }, print, model, config);
const WipeTowerData &data = print.wipe_tower_data(2);
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(data.depth, 1e-4));
CHECK(data.width > 0.f);
}
}
TEST_CASE("A single-filament plate reserves a tower only when one is actually printed", "[WipeTower]")
{
// Reporting no tower for one that is built collapses the validation hull to a point, so
// the config-visible reasons for a single-filament tower have to be honoured.
Print print;
Model model;
SECTION("no tool change and nothing else that prints one") {
const DynamicPrintConfig config = tower_estimate_config("rib");
init_print({ cube(20) }, print, model, config);
CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6));
}
SECTION("a raft puts the tower on every layer below the object") {
DynamicPrintConfig config = tower_estimate_config("rib");
config.set_deserialize_strict({ { "raft_layers", "3" } });
init_print({ cube(20) }, print, model, config);
CHECK(print.wipe_tower_data(1).depth > 0.f);
}
}

View File

@@ -40,6 +40,7 @@ add_executable(${_TEST_NAME}_tests
test_utils.cpp test_utils.cpp
test_timeutils.cpp test_timeutils.cpp
test_voronoi.cpp test_voronoi.cpp
test_wipe_tower_estimate.cpp
test_optimizers.cpp test_optimizers.cpp
test_ordering_strategies.cpp test_ordering_strategies.cpp
# test_png_io.cpp # test_png_io.cpp

View File

@@ -0,0 +1,206 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/GCode/WipeTower.hpp"
#include "libslic3r/GCode/WipeTower2.hpp"
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
#include "libslic3r/PrintConfig.hpp"
#include <cmath>
#include <string>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
// Rectangle wall, one nozzle, 100 mm3 prime volume on a 50 mm wide tower at 0.2 mm layers: one
// purge is 10 mm of depth. The flush matrix is off here; the shipped-default case covers it.
// Built as PresetBundle::full_config builds the GUI's: apply() creates each enum as a
// ConfigOptionEnumGeneric, where full_print_config() would clone the static defaults'
// ConfigOptionEnum<T>. The estimate has to read either.
static DynamicPrintConfig preset_shaped_defaults()
{
DynamicPrintConfig config;
config.apply(FullPrintConfig::defaults());
return config;
}
static DynamicPrintConfig make_config(const char *wall_type = "rectangle")
{
DynamicPrintConfig config = preset_shaped_defaults();
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
config.set_key_value("prime_volume", new ConfigOptionFloat(100.));
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(100.));
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(3.));
config.set_deserialize_strict("wipe_tower_wall_type", wall_type);
config.set_key_value("wipe_tower_rib_width", new ConfigOptionFloat(8.));
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
config.set_deserialize_strict("timelapse_type", "0");
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
config.set_key_value("raft_layers", new ConfigOptionInt(0));
config.set_key_value("purge_in_prime_tower", new ConfigOptionBool(false));
config.set_key_value("single_extruder_multi_material", new ConfigOptionBool(false));
return config;
}
TEST_CASE("A rectangle wall tower is sized by the purge volume", "[WipeTowerEstimate]") {
const DynamicPrintConfig config = make_config();
// Three filaments purge twice per layer; a 5 mm object keeps the stability floor at 5 mm.
const WipeTowerFootprint fp = estimate_wipe_tower_footprint(config, 3, 0.2, 5., false);
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
CHECK_THAT(fp.depth, WithinAbs(20., 1e-9));
CHECK_THAT(fp.height, WithinAbs(5., 1e-9));
CHECK_THAT(fp.brim_width, WithinAbs(3., 1e-9));
// Thinner layers need more depth for the same volume.
CHECK_THAT(estimate_wipe_tower_footprint(config, 3, 0.1, 5., false).depth, WithinAbs(40., 1e-9));
// The infill gap spaces the purge lines.
DynamicPrintConfig spaced = config;
spaced.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
CHECK_THAT(estimate_wipe_tower_footprint(spaced, 3, 0.2, 5., false).depth, WithinAbs(30., 1e-9));
}
TEST_CASE("Object height sets the stability floor and the auto brim", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
// Two filaments purge once: 10 mm, lifted to the 20 mm floor of a 100 mm tower.
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 100., false).depth, WithinAbs(20., 1e-9));
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(-1.));
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 50., false).brim_width, WithinAbs(WipeTower::get_auto_brim_by_height(50.f), 1e-6));
}
TEST_CASE("A single filament only gets a tower when one is printed anyway", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100., false).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate_wipe_tower_footprint(config, 0, 0.2, 100., false).width, WithinAbs(0., 1e-9));
// Wrapping detection prints a tower on the first layers whatever the filament count.
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100., false).depth, WithinAbs(20., 1e-9));
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
// So does a raft. raft_layers is a per-object key, so it arrives as a resolved flag and
// is deliberately not read off the config.
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100., true).depth, WithinAbs(20., 1e-9));
config.set_key_value("raft_layers", new ConfigOptionInt(3));
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100., false).depth, WithinAbs(0., 1e-9));
config.set_key_value("raft_layers", new ConfigOptionInt(0));
config.set_deserialize_strict("timelapse_type", "1");
// Smooth timelapse primes the single filament once: 10 mm, lifted to the floor.
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100., false).depth, WithinAbs(20., 1e-9));
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 5., false).depth, WithinAbs(10., 1e-9));
}
TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowerEstimate]") {
// A wall type may only change the shape of the tower, never whether one is reserved:
// reporting no tower for one that is built collapses the validation hull to a point.
const double height = GENERATE(5., 100.);
DynamicPrintConfig rect = make_config();
DynamicPrintConfig rib = make_config("rib");
// No tool change and nothing else that prints a tower - neither wall type reserves one.
CHECK_THAT(estimate_wipe_tower_footprint(rect, 1, 0.2, height, false).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height, false).depth, WithinAbs(0., 1e-9));
// Not even on a dual-nozzle printer, where a lone filament still needs no purge.
rect.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
rib.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
CHECK_THAT(estimate_wipe_tower_footprint(rect, 1, 0.2, height, false).depth, WithinAbs(0., 1e-9));
CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height, false).depth, WithinAbs(0., 1e-9));
// With a tool change both reserve one, and both respect the stability floor.
CHECK(estimate_wipe_tower_footprint(rect, 2, 0.2, height, false).depth >= WipeTower::get_limit_depth_by_height(float(height)));
CHECK(estimate_wipe_tower_footprint(rib, 2, 0.2, height, false).depth >= WipeTower::get_limit_depth_by_height(float(height)));
}
TEST_CASE("A rib wall squares the tower and caps the rib width", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config("rib");
// sqrt(200 / 0.2) = 31.62 mm square, plus the 8 mm rib bulge along the diagonal.
const double body = std::sqrt(1000.);
WipeTowerFootprint fp = estimate_wipe_tower_footprint(config, 3, 0.2, 5., false);
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + body, 1e-9));
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
// The extra rib length grows the footprint.
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(4.));
CHECK_THAT(estimate_wipe_tower_footprint(config, 3, 0.2, 5., false).depth, WithinAbs(8. / std::sqrt(2.) + body + 4., 1e-9));
// A tiny tower caps the rib width at half its depth: 5 mm body, 2.5 mm rib.
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
config.set_key_value("prime_volume", new ConfigOptionFloat(5.));
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5., false).depth, WithinAbs(2.5 / std::sqrt(2.) + 5., 1e-9));
}
TEST_CASE("Every wall and tower type is read the same from a preset and a static config", "[WipeTowerEstimate]") {
// The GUI, arrange and the CLI pass a DynamicPrintConfig whose enums are
// ConfigOptionEnumGeneric; Print passes a static config whose enums are ConfigOptionEnum<T>.
// The wall type is read by value, so both give the same shape, and the wipe tower
// implementation is not an input to the footprint at all.
const char *wall_type = GENERATE("rectangle", "cone", "rib");
const char *tower_type = GENERATE("type1", "type2");
DynamicPrintConfig preset = make_config(wall_type);
preset.set_deserialize_strict("wipe_tower_type", tower_type);
REQUIRE(dynamic_cast<const ConfigOptionEnumGeneric *>(preset.option("wipe_tower_wall_type")) != nullptr);
FullPrintConfig static_config;
static_config.apply(preset, true);
REQUIRE(static_config.wipe_tower_wall_type.serialize() == wall_type);
REQUIRE(static_config.wipe_tower_type.serialize() == tower_type);
// Three filaments purge twice per layer on a 5 mm object: a 50 x 20 rectangle, or a square.
const WipeTowerFootprint fp = estimate_wipe_tower_footprint(preset, 3, 0.2, 5., false);
if (std::string(wall_type) == "rib") {
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + std::sqrt(1000.), 1e-9));
} else {
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
CHECK_THAT(fp.depth, WithinAbs(20., 1e-9));
}
const WipeTowerFootprint from_static = estimate_wipe_tower_footprint(static_config, 3, 0.2, 5., false);
CHECK_THAT(from_static.width, WithinAbs(fp.width, 1e-9));
CHECK_THAT(from_static.depth, WithinAbs(fp.depth, 1e-9));
CHECK_THAT(from_static.brim_width, WithinAbs(fp.brim_width, 1e-9));
// Smooth timelapse is the other enum the estimate reads: a lone filament gets a tower
// through both storages too.
preset.set_deserialize_strict("timelapse_type", "1");
static_config.apply(preset, true);
CHECK(estimate_wipe_tower_footprint(preset, 1, 0.2, 5., false).depth > 0.);
CHECK(estimate_wipe_tower_footprint(static_config, 1, 0.2, 5., false).depth > 0.);
}
TEST_CASE("A dual nozzle purges every filament plus the filament change", "[WipeTowerEstimate]") {
DynamicPrintConfig config = make_config();
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("filament_change_length", new ConfigOptionFloats({10., 10.}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
// Two purges of 100 mm3 plus one 10 mm filament change: (200 + 10 * pi * 1.75^2 / 4) / (0.2 * 50).
const double change_volume = 10. * PI * 1.75 * 1.75 / 4.;
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5., false).depth, WithinAbs((200. + change_volume) / 10., 1e-9));
}
TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTowerEstimate]") {
// Both keys default to true, so the shipped configuration purges the flush volumes rather
// than the prime volume, with no infill gap on top - the flush volumes already hold it.
DynamicPrintConfig config = preset_shaped_defaults();
REQUIRE(config.opt_bool("purge_in_prime_tower"));
REQUIRE(config.opt_bool("single_extruder_multi_material"));
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
config.set_deserialize_strict("wipe_tower_wall_type", "rectangle");
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
const double flush_volume = WipeTower2::estimate_semm_flush_volume(config, 2);
const double expected = std::max(double(WipeTower::get_limit_depth_by_height(5.f)), flush_volume / (0.2 * 50.));
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5., false).depth, WithinAbs(expected, 1e-6));
}
TEST_CASE("A config missing a tower key falls back to that key's default", "[WipeTowerEstimate]") {
// The signature takes any ConfigBase: an absent key must read as its declared default.
const DynamicPrintConfig full = make_config();
DynamicPrintConfig partial = full;
partial.erase("prime_tower_infill_gap");
REQUIRE(partial.option("prime_tower_infill_gap") == nullptr);
DynamicPrintConfig defaulted = full;
defaulted.set_key_value("prime_tower_infill_gap",
print_config_def.get("prime_tower_infill_gap")->default_value->clone());
CHECK_THAT(estimate_wipe_tower_footprint(partial, 3, 0.2, 5., false).depth,
WithinAbs(estimate_wipe_tower_footprint(defaulted, 3, 0.2, 5., false).depth, 1e-9));
}