diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index 31f39921f4..5b1d2da1bf 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -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(); 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(); @@ -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_y = wipe_y_option->get_at(plate_index); - ConfigOptionFloat* width_option = print_config.option("prime_tower_width", true); - plate_obj_size_info.wipe_width = width_option->value; + // Body and brim from one estimate: resolving an auto (-1) brim against a different + // 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("prime_tower_brim_width", true); - float brim_width = brim_width_option->value; - 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("prime_volume", true); - float wipe_volume = volume_option->value; - - const ConfigOptionBool * wrapping_detection = print_config.option("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); + plate_obj_size_info.wipe_width = footprint.width; + plate_obj_size_info.wipe_depth = footprint.depth; 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); @@ -4875,7 +4866,7 @@ int CLI::run(int argc, char **argv) wipe_y_option->set_at(&wt_y_opt, i, 0); 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 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; - 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 if (bedid < plate_count) { @@ -5276,22 +5267,16 @@ int CLI::run(int argc, char **argv) //float depth = v * (filaments_cnt - 1) / (layer_height * w); - const ConfigOptionBool *wrapping_detection = m_print_config.option("enable_wrapping_detection"); - bool enable_wrapping = (wrapping_detection != nullptr) && wrapping_detection->value; - - Vec3d wipe_tower_size = cur_plate->estimate_wipe_tower_size(m_print_config, w, v, new_extruder_count, filaments_cnt, false, enable_wrapping); + const WipeTowerFootprint footprint = cur_plate->estimate_wipe_tower_footprint(m_print_config, filaments_cnt); + Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height); Vec3d plate_origin = cur_plate->get_origin(); int plate_width, plate_depth; double plate_height; partplate_list.get_plate_size(plate_width, plate_depth, plate_height); float depth = wipe_tower_size(1); - float margin = 15.f, wp_brim_width = 0.f; - ConfigOption *wipe_tower_brim_width_opt = m_print_config.option("prime_tower_brim_width"); - if (wipe_tower_brim_width_opt ) { - 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; - } + // Brim already resolved against the height the body was sized from. + float margin = 15.f, wp_brim_width = float(footprint.brim_width); + BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format("arrange wipe_tower: wp_brim_width %1%")%wp_brim_width; 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%") diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index d07c42d8f6..23b6decb2c 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -272,6 +272,8 @@ set(lisbslic3r_sources GCode/WipeTower2.hpp GCode/WipeTower.cpp GCode/WipeTower.hpp + GCode/WipeTowerEstimate.cpp + GCode/WipeTowerEstimate.hpp GCodeWriter.cpp GCodeWriter.hpp Geometry/ArcWelder.hpp diff --git a/src/libslic3r/GCode/WipeTowerEstimate.cpp b/src/libslic3r/GCode/WipeTowerEstimate.cpp new file mode 100644 index 0000000000..9e9bb7de4b --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerEstimate.cpp @@ -0,0 +1,109 @@ +#include "WipeTowerEstimate.hpp" + +#include "WipeTower.hpp" +#include "WipeTower2.hpp" +#include "../Config.hpp" +#include "../PrintConfig.hpp" +#include "../libslic3r.h" + +#include +#include + +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, 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(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(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 diff --git a/src/libslic3r/GCode/WipeTowerEstimate.hpp b/src/libslic3r/GCode/WipeTowerEstimate.hpp new file mode 100644 index 0000000000..911ca9560c --- /dev/null +++ b/src/libslic3r/GCode/WipeTowerEstimate.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +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 diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index aee50db534..7d362537ea 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -20,6 +20,7 @@ #include "GCode.hpp" #include "GCode/WipeTower.hpp" #include "GCode/WipeTower2.hpp" +#include "GCode/WipeTowerEstimate.hpp" #include "Utils.hpp" #include "PrintConfig.hpp" #include "MaterialType.hpp" @@ -1031,20 +1032,20 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, //BBS: add the wipe tower check logic 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(); const Vec3d plate_origin = print.get_plate_origin(); 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 width = config.prime_tower_width.value; float a = config.wipe_tower_rotation_angle.value; //float v = config.wiping_volume.value; - float depth = print.wipe_tower_data(filaments_count).depth; - //float brim_width = print.wipe_tower_data(filaments_count).brim_width; - - if (config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) - width = depth; + // The estimate resolves the effective width (a rib wall squares the tower). + const WipeTowerData &wipe_tower_estimate = print.wipe_tower_data(filaments_count); + float width = wipe_tower_estimate.width; + float depth = wipe_tower_estimate.depth; Polygons convex_hulls_temp; 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 { - // If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default. - double max_height = 0; - for (size_t obj_idx = 0; obj_idx < m_objects.size(); obj_idx++) { - double object_z = (double) m_objects[obj_idx]->size().z(); - max_height = std::max(unscale_(object_z), max_height); + // Until the tower is generated, size it with the estimate the GUI/CLI placement uses, so + // validation cannot reject a position the clamp just accepted. + if (is_step_done(psWipeTower) || filaments_cnt == 0) + return m_wipe_tower_data; + + double max_height = 0.; + double layer_height = std::numeric_limits::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 - layer_height = m_objects.front()->config().layer_height.value; - - auto timelapse_type = config().option>("timelapse_type"); - bool need_wipe_tower = (timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false) | (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib); - 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 filament_change_lengths; - auto filament_change_lengths_opt = config().option("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 diameters; - auto filament_diameter_opt = config().option("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(this)->m_wipe_tower_data.depth = depth; - const_cast(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(this)->m_wipe_tower_data.depth = depth; - const_cast(this)->m_wipe_tower_data.brim_width = m_config.prime_tower_brim_width; - } - if (m_config.prime_tower_brim_width < 0) const_cast(this)->m_wipe_tower_data.brim_width = WipeTower::get_auto_brim_by_height(max_height); - } + const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, filaments_cnt, layer_height, max_height, any_raft); + WipeTowerData &data = const_cast(this)->m_wipe_tower_data; + data.depth = float(footprint.depth); + data.width = float(footprint.width); + data.brim_width = float(footprint.brim_width); return m_wipe_tower_data; } diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 9e20061501..964deb7a60 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -782,6 +782,9 @@ struct WipeTowerData // Depth of the wipe tower to pass to GLCanvas3D for exact bounding box: 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> z_and_depth_pairs; float brim_width; float height; @@ -795,6 +798,7 @@ struct WipeTowerData used_filament.clear(); number_of_toolchanges = -1; depth = 0.f; + width = 0.f; brim_width = 0.f; height = 0.f; rib_offset = Vec2f::Zero(); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 6cdc27ed65..e1f47061c1 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2891,20 +2891,18 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re DynamicPrintConfig& proj_cfg = wxGetApp().preset_bundle->project_config; float x = dynamic_cast(proj_cfg.option("wipe_tower_x"))->get_at(plate_id); float y = dynamic_cast(proj_cfg.option("wipe_tower_y"))->get_at(plate_id); - float w = dynamic_cast(m_config->option("prime_tower_width"))->value; float a = dynamic_cast(m_config->option("wipe_tower_rotation_angle"))->value; - // BBS - float v = dynamic_cast(m_config->option("prime_volume"))->value; 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(); if (!need_wipe_tower && part_plate->get_extruders(true).size() < 2) continue; if (part_plate->get_objects_on_this_plate().empty()) continue; - float brim_width = print->wipe_tower_data(filaments_count).brim_width; - int nozzle_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - Vec3d wipe_tower_size = ppl.get_plate(plate_id)->estimate_wipe_tower_size(full_config, w, v, nozzle_nums, 0, false, dynamic_cast(dconfig.option("enable_wrapping_detection"))->value); + // Body and brim from this plate's own estimate: m_process->fff_print() is the + // selected plate's, so an auto brim drew every tower with that plate's brim. + 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 // set_default_wipe_tower_pos_for_plate and again on every drag. diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 6b74b71af1..c0cc7bdb5a 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -265,8 +265,7 @@ arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::setget_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, nozzle_nums, 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, extruder_size); arrange_poly.bed_idx = plate_index; return arrange_poly; } diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 849aa4e31a..3f56e9a8de 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -20,6 +20,7 @@ #include "libslic3r/libslic3r.h" #include "libslic3r/Polygon.hpp" +#include "libslic3r/GCode/WipeTowerEstimate.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/Geometry.hpp" @@ -1531,8 +1532,15 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const if (check_objects_empty_and_gcode3mf(plate_extruders)) { return plate_extruders; } - // if 3mf file - const DynamicPrintConfig& glb_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + return get_extruders(conside_custom_gcode, wxGetApp().preset_bundle->prints.get_edited_preset().config, wxGetApp().preset_bundle->project_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 PartPlate::get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const +{ + std::vector plate_extruders; int glb_support_intf_extr = glb_config.opt_int("support_interface_filament"); int glb_support_extr = glb_config.opt_int("support_filament"); int glb_outer_wall_extr = glb_config.opt_int("outer_wall_filament_id"); @@ -1549,7 +1557,9 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const glb_support |= glb_config.opt_int("raft_layers") > 0; 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; ModelObject* mo = m_model->objects[obj_idx]; @@ -1662,7 +1672,7 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const if (conside_custom_gcode) { //BBS int nums_extruders = 0; - if (const ConfigOptionStrings *color_option = dynamic_cast(wxGetApp().preset_bundle->project_config.option("filament_colour"))) { + if (const ConfigOptionStrings *color_option = dynamic_cast(project_config.option("filament_colour"))) { nums_extruders = color_option->values.size(); 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) { @@ -1681,9 +1691,8 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const // is never loaded into a tray, so callers (AMS mapping, filament checks) must see the // physical filaments it resolves to instead. { - auto& project_config = wxGetApp().preset_bundle->project_config; - auto* is_mixed_opt = project_config.option("filament_is_mixed"); - auto* comp_strs_opt = project_config.option("filament_mixed_components"); + const auto* is_mixed_opt = project_config.option("filament_is_mixed"); + const auto* comp_strs_opt = project_config.option("filament_mixed_components"); if (is_mixed_opt && comp_strs_opt && has_any_mixed_filament(is_mixed_opt->values)) { std::vector ext_0based; for (int e : plate_extruders) @@ -2311,113 +2320,86 @@ bool PartPlate::check_compatible_of_nozzle_and_filament(const DynamicPrintConfig 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; - double layer_height = 0.08f; // hard code layer height - double max_height = 0.f; - wipe_tower_size.setZero(); - - const ConfigOption* layer_height_opt = config.option("layer_height"); - if (layer_height_opt) - layer_height = layer_height_opt->getFloat(); - - // empty plate - if (plate_extruder_size == 0) - { - std::vector plate_extruders = get_extruders(true); - plate_extruder_size = plate_extruders.size(); + // The CLI calls this too, so the plate's filaments are derived from the passed config: + // get_extruders(bool) reads the same keys off wxGetApp()'s presets, which the CLI has none of. + std::vector plate_extruders; + if (plate_extruder_size == 0) { + plate_extruders = get_extruders(true, config, config); + plate_extruder_size = int(plate_extruders.size()); + } + // 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. + const ConfigOption *wipe_tower_filament_opt = config.option("wipe_tower_filament"); + 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) { + if (plate_extruders.empty()) + 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) - return wipe_tower_size; + return WipeTowerFootprint(); - for (int obj_idx = 0; obj_idx < m_model->objects.size(); obj_idx++) { - if (!use_global_objects && !contain_instance_totally(obj_idx, 0)) + // Tallest object on this plate and the thinnest layer it is sliced at, resolved per object + // 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::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; - - BoundingBoxf3 bbox = m_model->objects[obj_idx]->bounding_box_exact(); - max_height = std::max(bbox.size().z(), max_height); - } - wipe_tower_size(2) = max_height; - //const DynamicPrintConfig &dconfig = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto timelapse_type = config.option>("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* use_rib_wall_opt = config.option>("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 filament_change_lengths; - auto filament_change_lengths_opt = m_print->config().option("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 diameters; - auto filament_diameter_opt = m_print->config().option("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("purge_in_prime_tower"); - const auto *semm_opt = config.option("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; + // Per instance, to match PrintObject::size(); the union over instances differs once + // they are rotated apart. + for (int inst_idx = 0; inst_idx < int(object->instances.size()); ++inst_idx) { + if (!use_global_objects && !contain_instance_totally(obj_idx, inst_idx)) + continue; + max_height = std::max(max_height, object->instance_bounding_box(inst_idx, true).size().z()); } + 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 { - 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) { - 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; - } + if (layer_height == std::numeric_limits::max()) + layer_height = global_layer_height; - 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(config.option("wipe_tower_x"))->get_at(plate_index); float y = dynamic_cast(config.option("wipe_tower_y"))->get_at(plate_index); - float w = dynamic_cast(config.option("prime_tower_width"))->value; //float a = dynamic_cast(config.option("wipe_tower_rotation_angle"))->value; - float v = dynamic_cast(config.option("prime_volume"))->value; - float tower_brim_width = dynamic_cast(config.option("prime_tower_brim_width"))->value; - const ConfigOptionBool * wrapping_opt = dynamic_cast(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); + const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(config, plate_extruder_size, use_global_objects); + wt_size = Vec3d(footprint.width, footprint.depth, footprint.height); 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 margin = WIPE_TOWER_MARGIN + tower_brim_width, wp_brim_width = 0.f; - const ConfigOption* wipe_tower_brim_width_opt = config.option("prime_tower_brim_width"); - if (wipe_tower_brim_width_opt) { - wp_brim_width = wipe_tower_brim_width_opt->getFloat(); - 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; - } + // Resolved brim, not the raw option: "Auto" (-1) would yield a margin of 0 and let the + // clamp put the brim off the bed. Matches set_default_wipe_tower_pos_for_plate. + const float wp_brim_width = float(footprint.brim_width); + const float margin = WIPE_TOWER_MARGIN + 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); - y = std::clamp(y, margin, (float)plate_depth - depth - margin - wp_brim_width); + // A tower too deep for the plate leaves no valid position: clamping with hi < lo is UB and + // 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(1) = y; wt_pos(2) = 0.f; @@ -2755,6 +2737,20 @@ bool PartPlate::contain_instance_totally(int obj_id, int instance_id) const 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 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); } DynamicPrintConfig full_config = wxGetApp().preset_bundle->full_config(false, filament_maps, f_volume_maps); - float w = dynamic_cast(full_config.option("prime_tower_width"))->value; - float v = dynamic_cast(full_config.option("prime_volume"))->value; - bool enable_wrapping = false; - const ConfigOptionBool *wrapping_opt = dynamic_cast(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); + WipeTowerFootprint footprint = part_plate->estimate_wipe_tower_footprint(full_config, init_pos ? 2 : 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(full_config, w, v, nozzle_nums, 2, false, enable_wrapping); + if (!init_pos && (is_approx(footprint.width, 0.0) || is_approx(footprint.depth, 0.0))) { + 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 - float brim_width = 0.f; - const ConfigOptionFloat *brim_opt = full_config.option("prime_tower_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; + // Brim-aware margin: the brim extends outward from the tower position. + const float brim_width = float(footprint.brim_width); + const float margin = WIPE_TOWER_MARGIN + brim_width; // clamp wipe tower position within plate boundaries { diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 8ad2f4a7d1..1cd45f77fd 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -11,6 +11,7 @@ #include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Slicing.hpp" +#include "libslic3r/GCode/WipeTowerEstimate.hpp" #include "libslic3r/Arrange.hpp" #include "Plater.hpp" #include "libslic3r/Model.hpp" @@ -339,11 +340,14 @@ public: 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 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; - 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; + // plate_extruder_size: filaments purged on the plate; 0 derives it from the plate's objects. + 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 &result) const; // get used filaments from config, 1 based idx std::vector get_extruders(bool conside_custom_gcode = false) const; + std::vector get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const; std::vector get_extruders_under_cli(bool conside_custom_gcode, DynamicPrintConfig& full_config) const; std::vector get_extruders_without_support(bool conside_custom_gcode = false) const; // get used filaments from gcode result, 1 based idx @@ -366,6 +370,8 @@ public: bool contain_instance_totally(ModelObject* object, int instance_id) const; //judge whether instance is totally included in plate or not 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 bool is_left_top_of(int obj_id, int instance_id); diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index 2bd7ac4189..d5f56d3f6c 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -182,3 +182,86 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor", 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> 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); + } +} diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 0d29ea11ae..dc8508743e 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -40,6 +40,7 @@ add_executable(${_TEST_NAME}_tests test_utils.cpp test_timeutils.cpp test_voronoi.cpp + test_wipe_tower_estimate.cpp test_optimizers.cpp test_ordering_strategies.cpp # test_png_io.cpp diff --git a/tests/libslic3r/test_wipe_tower_estimate.cpp b/tests/libslic3r/test_wipe_tower_estimate.cpp new file mode 100644 index 0000000000..753d872f29 --- /dev/null +++ b/tests/libslic3r/test_wipe_tower_estimate.cpp @@ -0,0 +1,206 @@ +#include + +#include "libslic3r/GCode/WipeTower.hpp" +#include "libslic3r/GCode/WipeTower2.hpp" +#include "libslic3r/GCode/WipeTowerEstimate.hpp" +#include "libslic3r/PrintConfig.hpp" + +#include +#include + +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. 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. + // 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(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)); +}