From e1efec7d6ce537c2408fd6ada5aefac7a909a685 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Tue, 8 Sep 2026 14:43:23 +0800 Subject: [PATCH] Fix review findings in the shared wipe tower estimate A raft is not a reason to reserve a tower. Print::apply runs normalize_fdm_2, which clears enable_prime_tower for a plate that purges one filament unless smooth timelapse or wrapping detection is on, so a single-filament plate with a raft prints no tower at all and the estimate was reserving bed area for one. Drop the input; need_wipe_tower is now exactly the two exceptions normalize_fdm_2 honours, named there so the next reason added has to be checked against it. The GUI preview and the validation containment check each re-derived "is a tower printed here" from the filament count instead of reading the estimate, so both missed the towers printed with no tool change to purge for. They now take the answer from the footprint, which is the drift this shared estimate exists to remove. A tower that is not printed estimates to zero, so its hull is degenerate and every check on it passes trivially - the containment check needs no gate of its own. WipeTowerData::width was written only by the pre-generation estimate and left at zero for the whole post-generation life of the Print, while its neighbour depth held the real value. Set it from the generator in both branches. The plate's height scan transformed every model part's full mesh per instance on each scene reload, discarding all but the z extent. The cached convex hull has the same z extent. A plate loaded from a sliced .gcode.3mf holds no objects and its filaments live in slice_filaments_info; the config-taking get_extruders overload returned an empty list for it, which sized the tower for a placeholder two filaments. It now answers the way the wx overload does, without reaching the plater. Also drop estimate_wipe_tower_size, which has no callers. --- src/libslic3r/GCode/WipeTowerEstimate.cpp | 11 +-- src/libslic3r/GCode/WipeTowerEstimate.hpp | 8 +-- src/libslic3r/Print.cpp | 29 ++++---- src/libslic3r/Print.hpp | 4 +- src/slic3r/GUI/GLCanvas3D.cpp | 4 +- src/slic3r/GUI/PartPlate.cpp | 27 ++++--- src/slic3r/GUI/PartPlate.hpp | 5 +- tests/fff_print/test_wipe_tower.cpp | 67 ++++++++++++++++- tests/libslic3r/test_wipe_tower_estimate.cpp | 75 ++++++++++++-------- 9 files changed, 154 insertions(+), 76 deletions(-) diff --git a/src/libslic3r/GCode/WipeTowerEstimate.cpp b/src/libslic3r/GCode/WipeTowerEstimate.cpp index 9e9bb7de4b..a8aeda28ef 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.cpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.cpp @@ -11,7 +11,7 @@ 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 estimate_wipe_tower_footprint(const ConfigBase &config, size_t filaments_cnt, double layer_height, double max_object_height) { WipeTowerFootprint footprint; footprint.height = max_object_height; @@ -56,8 +56,9 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, size_ 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; + // Reasons a tower is printed with no tool change to purge for: the ones that stop + // normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled. + const bool need_wipe_tower = smooth_timelapse || opt_bool("enable_wrapping_detection"); // No tool change, nothing to purge; smooth timelapse still primes once. size_t purge_count = 0; @@ -80,7 +81,9 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, size_ // 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) + // A tool change is a reason on its own: the generator floors the tower whatever the + // purge volumes resolve to. + if (volume < EPSILON && filaments_cnt < 2 && !need_wipe_tower) return footprint; const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height)); diff --git a/src/libslic3r/GCode/WipeTowerEstimate.hpp b/src/libslic3r/GCode/WipeTowerEstimate.hpp index 911ca9560c..fe5c0b519c 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.hpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.hpp @@ -21,9 +21,9 @@ struct WipeTowerFootprint // 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); +// +// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate +// purging one filament unless smooth timelapse or wrapping detection is on. +WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, size_t filaments_cnt, double layer_height, double max_object_height); } // namespace Slic3r diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 7d362537ea..39410a8cef 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1081,22 +1081,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print, if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) { return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")}; } - // Skip the containment check for towers that will never be printed (single-filament - // prints without smooth timelapse keep the config's tower position but emit nothing). + // No gate on "is there a tower": one that is not printed estimates to zero, so the hull + // is degenerate and every check passes. Re-deriving it here missed the wrapping-detection + // tower on a single-filament plate. // Pre-generation only the body square is tested — the auto-brim estimate can overshoot // the generated brim by several mm and must not hard-fail a print that physically fits. // Post-generation the mesh bottom already includes the real brim, so the exact // footprint is tested. - if (filaments_count > 1 || print.enable_timelapse_print()) { - // The shared printable polygon is plate-local, while the tower polygons above are - // already shifted by the plate origin. - Polygons printable_polys = print.get_extruder_shared_printable_polygon(); - const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); - for (Polygon &p : printable_polys) - p.translate(plate_shift); - if (!diff(convex_hulls_temp, printable_polys).empty()) - return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; - } + // The shared printable polygon is plate-local, while the tower polygons above are + // already shifted by the plate origin. + Polygons printable_polys = print.get_extruder_shared_printable_polygon(); + const Point plate_shift(scale_(plate_origin.x()), scale_(plate_origin.y())); + for (Polygon &p : printable_polys) + p.translate(plate_shift); + if (!diff(convex_hulls_temp, printable_polys).empty()) + return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")}; return {}; } @@ -4005,16 +4004,14 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const 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; - const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, filaments_cnt, layer_height, max_height, any_raft); + const WipeTowerFootprint footprint = estimate_wipe_tower_footprint(m_config, filaments_cnt, layer_height, max_height); WipeTowerData &data = const_cast(this)->m_wipe_tower_data; data.depth = float(footprint.depth); data.width = float(footprint.width); @@ -4244,6 +4241,7 @@ void Print::_make_wipe_tower() m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); wipe_tower.generate_new(m_wipe_tower_data.tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); + m_wipe_tower_data.width = wipe_tower.width(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.bbx = wipe_tower.get_bbx(); m_wipe_tower_data.rib_offset = wipe_tower.get_rib_offset(); @@ -4357,6 +4355,7 @@ void Print::_make_wipe_tower() m_wipe_tower_data.tool_changes.reserve(m_wipe_tower_data.tool_ordering.layer_tools().size()); wipe_tower.generate(m_wipe_tower_data.tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); + m_wipe_tower_data.width = wipe_tower.width(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 964deb7a60..af1dc3af40 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -782,8 +782,8 @@ 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. + // Effective width (a rib wall squares the tower): the estimate until generation, then the + // generated width, so it never disagrees with depth. float width; std::vector> z_and_depth_pairs; float brim_width; diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e1f47061c1..b0675360b8 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2895,12 +2895,14 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re Vec3d plate_origin = ppl.get_plate(plate_id)->get_origin(); 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; // 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); + // The estimate is also the answer to whether this plate prints a tower; + // deciding it here as well only gave the two room to drift. + if (footprint.depth <= 0.) continue; float brim_width = float(footprint.brim_width); Vec3d wipe_tower_size(footprint.width, footprint.depth, footprint.height); diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 3f56e9a8de..09b8ff3068 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -1541,6 +1542,14 @@ std::vector PartPlate::get_extruders(bool conside_custom_gcode) const std::vector PartPlate::get_extruders(bool conside_custom_gcode, const DynamicPrintConfig& glb_config, const DynamicPrintConfig& project_config) const { std::vector plate_extruders; + // A plate from a sliced .gcode.3mf holds no objects, so report the filaments the G-code + // used. check_objects_empty_and_gcode3mf does this for get_extruders(bool), but reaches + // the plater, which the CLI has none of; slice_filaments_info is only filled for such a plate. + if (m_model->objects.empty()) { + for (const FilamentInfo &info : slice_filaments_info) + plate_extruders.push_back(info.id + 1); + return 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"); @@ -2347,37 +2356,27 @@ WipeTowerFootprint PartPlate::estimate_wipe_tower_footprint(const DynamicPrintCo // 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; // Per instance, to match PrintObject::size(); the union over instances differs once - // they are rotated apart. + // they are rotated apart. The cached convex hull has the mesh's z extent and is cheap + // enough for every scene reload. 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()); + max_height = std::max(max_height, object->instance_convex_hull_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; } if (layer_height == std::numeric_limits::max()) layer_height = global_layer_height; - return Slic3r::estimate_wipe_tower_footprint(config, size_t(plate_extruder_size), layer_height, max_height, any_raft); -} - -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); + return Slic3r::estimate_wipe_tower_footprint(config, size_t(plate_extruder_size), layer_height, max_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 diff --git a/src/slic3r/GUI/PartPlate.hpp b/src/slic3r/GUI/PartPlate.hpp index 1cd45f77fd..829d417e04 100644 --- a/src/slic3r/GUI/PartPlate.hpp +++ b/src/slic3r/GUI/PartPlate.hpp @@ -340,9 +340,10 @@ 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; - // plate_extruder_size: filaments purged on the plate; 0 derives it from the plate's objects. + // plate_extruder_size: filaments purged on the plate; 0 derives them from its objects. + // use_global_objects skips the containment test, which the CLI needs before objects are + // assigned to plates - the layer height is then the project's thinnest, which over-reserves. 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 diff --git a/tests/fff_print/test_wipe_tower.cpp b/tests/fff_print/test_wipe_tower.cpp index d5f56d3f6c..9a6c5aa686 100644 --- a/tests/fff_print/test_wipe_tower.cpp +++ b/tests/fff_print/test_wipe_tower.cpp @@ -199,6 +199,7 @@ static DynamicPrintConfig tower_estimate_config(const char *wall_type) { "single_extruder_multi_material", "0" }, { "timelapse_type", "0" }, { "layer_height", "0.2" }, + { "enable_wrapping_detection", "0" }, { "raft_layers", "0" } }); } @@ -245,23 +246,83 @@ TEST_CASE("Validation is given the tower's effective width, not the configured o } } +TEST_CASE("Generating the tower keeps its reported width current", "[WipeTower]") +{ + // width is handed out after the slice, so leaving it at the estimate reports a zero-width + // tower to every post-generation consumer. + const DynamicPrintConfig config = wipe_tower_toolchange_config("marlin"); + Print print; + Model model; + init_print({ cube(10) }, print, model, config); + print.apply(model, config); + REQUIRE(print.wipe_tower_data(2).width > 0.f); + + print.process(); + REQUIRE(print.is_step_done(psWipeTower)); + const WipeTowerData &data = print.wipe_tower_data(); + // A width the generator never wrote reads as zero. A rib wall squares the tower, so the + // generated width is the body square: under the configured 50 mm, and inside the depth. + CHECK(data.width > 0.f); + CHECK(data.width < 50.f); + CHECK(data.width <= data.depth + EPSILON); +} + 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. + // The estimate has to answer this the way Print::apply does: reporting no tower for one + // that is built collapses the validation hull to a point, and reporting one for a tower + // that is not built takes that bed area away from the arranger and draws a preview box + // over nothing. 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); + REQUIRE_FALSE(print.has_wipe_tower()); 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") { + // A raft puts the tower on every layer below the object, but only where there is a tower: + // Print::apply runs normalize_fdm_2, which clears enable_prime_tower for a plate that + // purges one filament and has neither smooth timelapse nor wrapping detection on. + SECTION("a raft alone does not print one") { DynamicPrintConfig config = tower_estimate_config("rib"); config.set_deserialize_strict({ { "raft_layers", "3" } }); init_print({ cube(20) }, print, model, config); + REQUIRE_FALSE(print.config().enable_prime_tower.value); + REQUIRE_FALSE(print.has_wipe_tower()); + CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6)); + } + + SECTION("smooth timelapse prints one, and keeps enable_prime_tower on") { + DynamicPrintConfig config = tower_estimate_config("rib"); + config.set_deserialize_strict({ { "timelapse_type", "1" } }); + init_print({ cube(20) }, print, model, config); + REQUIRE(print.has_wipe_tower()); CHECK(print.wipe_tower_data(1).depth > 0.f); } } + +TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]") +{ + // Wrapping detection prints a tower on a plate that purges one filament. Neither the old + // estimate (which read the wall type and smooth timelapse) nor the old containment gate (the + // filament count or smooth timelapse) knew about it, so between them that tower was never + // checked against the bed. + Print print; + Model model; + DynamicPrintConfig config = tower_estimate_config("rectangle"); + // Relative E without a per-layer G92 is rejected before the tower is ever looked at, and + // has_wipe_tower() wants a real exclusion polygon before it honours wrapping detection. + config.set_deserialize_strict({ { "enable_wrapping_detection", "1" }, + { "wrapping_exclude_area", "180x180,190x180,190x190,180x190" }, + { "wipe_tower_x", "500" }, { "wipe_tower_y", "500" }, + { "use_relative_e_distances", "0" } }); + + init_print({ cube(20) }, print, model, config); + REQUIRE(print.extruders(true).size() == 1); + REQUIRE(print.has_wipe_tower()); + CHECK(print.wipe_tower_data(1).depth > 0.f); + CHECK_THAT(print.validate().string, Catch::Matchers::ContainsSubstring("printable area")); +} diff --git a/tests/libslic3r/test_wipe_tower_estimate.cpp b/tests/libslic3r/test_wipe_tower_estimate.cpp index 753d872f29..00235bb2ff 100644 --- a/tests/libslic3r/test_wipe_tower_estimate.cpp +++ b/tests/libslic3r/test_wipe_tower_estimate.cpp @@ -45,48 +45,61 @@ static DynamicPrintConfig make_config(const char *wall_type = "rectangle") 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); + const WipeTowerFootprint fp = estimate_wipe_tower_footprint(config, 3, 0.2, 5.); 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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 3, 0.1, 5.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(spaced, 3, 0.2, 5.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 100.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 50.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 0, 0.2, 100.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100.).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)); + // A raft is not one of them: normalize_fdm_2 clears enable_prime_tower for a plate that + // purges one filament unless smooth timelapse or wrapping detection is on, so a raft + // alone leaves no tower to reserve for. 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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 5.).depth, WithinAbs(10., 1e-9)); +} + +TEST_CASE("A tool change reserves the stability floor even with nothing to purge", "[WipeTowerEstimate]") { + // The purge volumes are configurable down to zero, but the tool changes are still printed on + // the tower and the generator still floors it, so the estimate has to floor it too. + const double height = GENERATE(5., 100.); + const float floor = WipeTower::get_limit_depth_by_height(float(height)); + DynamicPrintConfig config = make_config(GENERATE("rectangle", "rib")); + config.set_key_value("prime_volume", new ConfigOptionFloat(0.)); + + CHECK(estimate_wipe_tower_footprint(config, 3, 0.2, height).depth >= floor); + // Still nothing for a lone filament with no other reason. + CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, height).depth, WithinAbs(0., 1e-9)); } TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowerEstimate]") { @@ -97,34 +110,34 @@ TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowe 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)); + CHECK_THAT(estimate_wipe_tower_footprint(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9)); + CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height).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)); + CHECK_THAT(estimate_wipe_tower_footprint(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9)); + CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height).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))); + CHECK(estimate_wipe_tower_footprint(rect, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height))); + CHECK(estimate_wipe_tower_footprint(rib, 2, 0.2, height).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); + WipeTowerFootprint fp = estimate_wipe_tower_footprint(config, 3, 0.2, 5.); 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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 3, 0.2, 5.).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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5.).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]") { @@ -144,7 +157,7 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static 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); + const WipeTowerFootprint fp = estimate_wipe_tower_footprint(preset, 3, 0.2, 5.); 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)); @@ -153,7 +166,7 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static CHECK_THAT(fp.depth, WithinAbs(20., 1e-9)); } - const WipeTowerFootprint from_static = estimate_wipe_tower_footprint(static_config, 3, 0.2, 5., false); + const WipeTowerFootprint from_static = estimate_wipe_tower_footprint(static_config, 3, 0.2, 5.); 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)); @@ -162,8 +175,8 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static // 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.); + CHECK(estimate_wipe_tower_footprint(preset, 1, 0.2, 5.).depth > 0.); + CHECK(estimate_wipe_tower_footprint(static_config, 1, 0.2, 5.).depth > 0.); } TEST_CASE("A dual nozzle purges every filament plus the filament change", "[WipeTowerEstimate]") { @@ -173,7 +186,7 @@ TEST_CASE("A dual nozzle purges every filament plus the filament change", "[Wipe 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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5.).depth, WithinAbs((200. + change_volume) / 10., 1e-9)); } TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTowerEstimate]") { @@ -188,7 +201,7 @@ TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTow 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)); + CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 5.).depth, WithinAbs(expected, 1e-6)); } TEST_CASE("A config missing a tower key falls back to that key's default", "[WipeTowerEstimate]") { @@ -201,6 +214,6 @@ TEST_CASE("A config missing a tower key falls back to that key's default", "[Wip 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)); + CHECK_THAT(estimate_wipe_tower_footprint(partial, 3, 0.2, 5.).depth, + WithinAbs(estimate_wipe_tower_footprint(defaulted, 3, 0.2, 5.).depth, 1e-9)); }