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)); }