#include #include "libslic3r/ExtrusionEntity.hpp" #include "libslic3r/ExtrusionEntityCollection.hpp" #include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" #include #include #include #include "test_helpers.hpp" using namespace Slic3r; using namespace Slic3r::Test; namespace { // The layer at this Z is the last one of the base, so its top surface is the ledge. const double ledge_z = 5.0; // The first layer, at initial_layer_print_height. const double first_layer_z = 0.2; // TestMesh::step scaled 3x in X/Y: a 60x60x5 base carrying a 54x54 column up to z=10, leaving a 3mm // top ledge around a feature that keeps rising. That is the geometry both only_one_wall_top and the // top surface expansion act on. The ledge has to stay wider than the wall band plus two top-infill // lines, or the expansion discards it as a sliver and the tests below assert nothing. TriangleMesh step_with_ledge() { TriangleMesh m = Slic3r::Test::mesh(TestMesh::step); m.scale(Vec3f(3.f, 3.f, 1.f)); return m; } // Every setting the assertions depend on, so none of them rests on a default. DynamicPrintConfig base_config(const char *wall_generator) { DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); config.set_deserialize_strict({ { "wall_generator", wall_generator }, { "layer_height", 0.2 }, // puts a layer boundary exactly on ledge_z { "initial_layer_print_height", 0.2 }, { "wall_loops", 3 }, { "sparse_infill_density", "15%" }, { "top_shell_layers", 3 }, { "bottom_shell_layers", 3 }, { "top_surface_density", "100%" }, { "top_surface_expansion", 0.0 }, { "only_one_wall_top", false }, { "only_one_wall_first_layer", false }, // Do not let the one-wall threshold discard the 3mm ledge before the feature sees it. { "min_width_top_surface", 0.0 }, }); return config; } double collection_length(const ExtrusionEntityCollection &coll) { double len = 0.; for (const ExtrusionEntity *entity : coll.flatten().entities) if (! entity->is_collection()) len += entity->length(); return len; } // Extruded length per layer. Two slices are compared through this rather than through their G-code, // because the G-code carries a config block that differs whenever any setting differs. struct SliceLengths { std::vector perimeters; std::vector fills; }; SliceLengths slice_lengths(const Print &print) { SliceLengths out; for (const Layer *layer : print.objects().front()->layers()) { double perimeters = 0., fills = 0.; for (const LayerRegion *region : layer->regions()) { perimeters += collection_length(region->perimeters); fills += collection_length(region->fills); } out.perimeters.push_back(perimeters); out.fills.push_back(fills); } return out; } double perimeter_length_at(const Print &print, double print_z) { for (const Layer *layer : print.objects().front()->layers()) if (std::abs(layer->print_z - print_z) < 1e-4) { double len = 0.; for (const LayerRegion *region : layer->regions()) len += collection_length(region->perimeters); return len; } return 0.; } // Largest per-layer difference between two series; a negative result means they are not comparable. double max_difference(const std::vector &a, const std::vector &b) { if (a.size() != b.size() || a.empty()) return -1.; double worst = 0.; for (size_t i = 0; i < a.size(); ++ i) worst = std::max(worst, std::abs(a[i] - b[i])); return worst; } } // namespace // The expansion only retypes area as top solid infill, so it can do nothing where there is no top // fill to begin with: zero top shell layers retypes the top surfaces as internal, and a top surface // density of 0% leaves the top layer with walls only. The last section is the control - the same // expansion on the same model does change the slice once a top fill exists - without which the two // equality checks above it would hold for an unrelated reason. TEST_CASE("Top surface expansion only acts where there is a top fill", "[Perimeters]") { const char *wall_generator = GENERATE("classic", "arachne"); CAPTURE(wall_generator); auto lengths_for = [wall_generator](int top_shell_layers, const char *top_surface_density, double expansion) { DynamicPrintConfig config = base_config(wall_generator); config.set_deserialize_strict({ { "top_shell_layers", top_shell_layers }, { "top_surface_density", top_surface_density }, { "top_surface_expansion", expansion }, }); Print print; init_and_process_print({ step_with_ledge() }, print, config); REQUIRE_FALSE(print.objects().empty()); return slice_lengths(print); }; SECTION("no top shell layers") { const SliceLengths off = lengths_for(0, "100%", 0.0); const SliceLengths on = lengths_for(0, "100%", 2.0); REQUIRE(off.perimeters.size() == on.perimeters.size()); CHECK_THAT(max_difference(off.perimeters, on.perimeters), Catch::Matchers::WithinAbs(0., 1.0)); CHECK_THAT(max_difference(off.fills, on.fills), Catch::Matchers::WithinAbs(0., 1.0)); } SECTION("zero top surface density") { const SliceLengths off = lengths_for(3, "0%", 0.0); const SliceLengths on = lengths_for(3, "0%", 2.0); REQUIRE(off.perimeters.size() == on.perimeters.size()); CHECK_THAT(max_difference(off.perimeters, on.perimeters), Catch::Matchers::WithinAbs(0., 1.0)); CHECK_THAT(max_difference(off.fills, on.fills), Catch::Matchers::WithinAbs(0., 1.0)); } SECTION("with a top fill the same expansion does change the slice") { const SliceLengths off = lengths_for(3, "100%", 0.0); const SliceLengths on = lengths_for(3, "100%", 2.0); REQUIRE(off.fills.size() == on.fills.size()); CHECK(max_difference(off.fills, on.fills) > scale_(0.5)); } } // With no top shell the top surfaces are retyped as internal, so the top surface density has nothing // left to control: there is no top fill, and only_one_wall_top - the one route from the density to the // perimeters - is itself switched off for want of a top surface to act on. TEST_CASE("Top surface density does not affect a slice without a top shell", "[Perimeters]") { const char *wall_generator = GENERATE("classic", "arachne"); CAPTURE(wall_generator); auto lengths_for = [wall_generator](const char *top_surface_density) { DynamicPrintConfig config = base_config(wall_generator); config.set_deserialize_strict({ { "top_shell_layers", 0 }, { "only_one_wall_top", true }, { "top_surface_density", top_surface_density }, }); Print print; init_and_process_print({ step_with_ledge() }, print, config); REQUIRE_FALSE(print.objects().empty()); return slice_lengths(print); }; const SliceLengths solid = lengths_for("100%"); const SliceLengths none = lengths_for("0%"); REQUIRE(solid.perimeters.size() == none.perimeters.size()); CHECK_THAT(max_difference(solid.perimeters, none.perimeters), Catch::Matchers::WithinAbs(0., 1.0)); CHECK_THAT(max_difference(solid.fills, none.fills), Catch::Matchers::WithinAbs(0., 1.0)); } // On the ledge layer the inner walls are given up to the top fill, so that layer loses wall length. // The handover needs a top fill that reaches the freed space: at a top surface density of 0% there is // no top fill at all, and without top_surface_expansion the fill never grows over the walls. Either // way the feature still runs, through the original generation, which keeps the inner walls up to the // top boundary - putting that layer back between the plain and the one-wall slice. TEST_CASE("Only one wall on top surfaces drops inner walls only where a top fill replaces them", "[Perimeters]") { const char *wall_generator = GENERATE("classic", "arachne"); CAPTURE(wall_generator); auto ledge_perimeters_for = [wall_generator](bool only_one_wall_top, const char *top_surface_density, double expansion) { DynamicPrintConfig config = base_config(wall_generator); config.set_deserialize_strict({ { "only_one_wall_top", only_one_wall_top }, { "top_surface_density", top_surface_density }, { "top_surface_expansion", expansion }, }); Print print; init_and_process_print({ step_with_ledge() }, print, config); REQUIRE_FALSE(print.objects().empty()); return perimeter_length_at(print, ledge_z); }; const double plain = ledge_perimeters_for(false, "100%", 2.0); const double one_wall = ledge_perimeters_for(true, "100%", 2.0); const double one_wall_no_fill = ledge_perimeters_for(true, "0%", 2.0); const double one_wall_no_expand = ledge_perimeters_for(true, "100%", 0.0); REQUIRE(plain > 0.); CHECK(one_wall < plain); // Both fall back to the original generation, which cuts the walls back to the top boundary but not past it. CHECK(one_wall_no_fill > one_wall); CHECK(one_wall_no_fill < plain); CHECK(one_wall_no_expand > one_wall); CHECK(one_wall_no_expand < plain); } // The bottom counterpart: the first layer is thinned to a single wall only where a bottom shell fills the // space behind it. With no bottom shell layers the bottom surfaces are retyped as internal, so that wall // would ring sparse infill on the bed - the option is switched off instead, and the GUI hides it in that // state so a profile that left it enabled cannot act behind a hidden checkbox. TEST_CASE("Only one wall on the first layer needs a bottom shell", "[Perimeters]") { const char *wall_generator = GENERATE("classic", "arachne"); CAPTURE(wall_generator); auto first_layer_perimeters_for = [wall_generator](bool only_one_wall_first_layer, int bottom_shell_layers) { DynamicPrintConfig config = base_config(wall_generator); config.set_deserialize_strict({ { "only_one_wall_first_layer", only_one_wall_first_layer }, { "bottom_shell_layers", bottom_shell_layers }, }); Print print; init_and_process_print({ step_with_ledge() }, print, config); REQUIRE_FALSE(print.objects().empty()); return perimeter_length_at(print, first_layer_z); }; const double plain = first_layer_perimeters_for(false, 3); const double one_wall = first_layer_perimeters_for(true, 3); // Both at zero bottom shell layers, so everything else that setting changes cancels out between them. const double plain_no_shell = first_layer_perimeters_for(false, 0); const double one_wall_no_shell = first_layer_perimeters_for(true, 0); REQUIRE(plain > 0.); CHECK(one_wall < plain); // No bottom shell: the option is inert, down to the same walls an unchecked box gives. CHECK_THAT(one_wall_no_shell, Catch::Matchers::WithinAbs(plain_no_shell, 1.0)); }