diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 063481876f..f29d4ef3fd 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -57,6 +57,9 @@ double calculate_infill_rotation_angle(const PrintObject* object, if (template_string.empty()) { return Geometry::deg2rad(fixed_infill_angle); } + // Convert the id to an index. Layer::id() counts the raft layers, object->layers() does not. + const size_t first_object_layer_id = object->get_layer(0)->id(); + layer_id = layer_id > first_object_layer_id ? layer_id - first_object_layer_id : 0; double angle = 0.0; ConfigOptionFloats rotate_angles; const std::string search_string = "/NnZz$LlUuQq~^|#"; @@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object, double angle_start = 0; double limit_fill_z = object->get_layer(0)->bottom_z(); double start_fill_z = limit_fill_z; + // The raft height, or 0 without a raft. + const double print_z_offset = object->slicing_parameters().object_print_z_min; bool _noop = false; auto fill_form = std::string::npos; bool _absolute = false; @@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object, for (int i = 0; i <= layer_id; i++) { double fill_z = object->get_layer(i)->bottom_z(); - if (limit_fill_z < object->get_layer(i)->slice_z) { + // slice_z is measured from the bottom of the model, limit_fill_z from the build plate. + if (limit_fill_z < object->get_layer(i)->slice_z + print_z_offset) { if (repeats) { // if repeats >0 then restore parameters for new iteration limit_fill_z += limit_fill_z - start_fill_z; start_fill_z = fill_z; diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index cc3757a02d..5fbce5a342 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -1,12 +1,18 @@ #include +#include +#include +#include #include #include +#include +#include #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Flow.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/Layer.hpp" #include "libslic3r/Print.hpp" #include "libslic3r/SVG.hpp" #include "libslic3r/libslic3r.h" @@ -476,3 +482,219 @@ bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacin return uncovered.empty(); // solid surface is fully filled } + +// Length-weighted dominant direction of the layer's role_wanted extrusions, whole degrees +// [0, 180), or -1 if it has none. Needs a line pattern such as monotonic or rectilinear. +template static int dominant_fill_angle(const Layer &layer, RolePred role_wanted) +{ + std::map weight_per_degree; + + auto account = [&weight_per_degree, &role_wanted](const ExtrusionPath &path) { + if (!role_wanted(path.role())) + return; + const Points3 &pts = path.polyline.points; + for (size_t i = 1; i < pts.size(); ++i) { + const double dx = double(pts[i].x() - pts[i - 1].x()); + const double dy = double(pts[i].y() - pts[i - 1].y()); + const double len = std::hypot(dx, dy); + if (len <= 0.) + continue; + int deg = int(std::lround(Geometry::rad2deg(std::atan2(dy, dx)))) % 180; + if (deg < 0) + deg += 180; + weight_per_degree[deg] += len; + } + }; + + for (const LayerRegion *region : layer.regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) { + if (auto *path = dynamic_cast(entity)) + account(*path); + else if (auto *multi = dynamic_cast(entity)) + for (const ExtrusionPath &p : multi->paths) + account(p); + else if (auto *loop = dynamic_cast(entity)) + for (const ExtrusionPath &p : loop->paths) + account(p); + } + + if (weight_per_degree.empty()) + return -1; + return std::max_element(weight_per_degree.begin(), weight_per_degree.end(), + [](const auto &a, const auto &b) { return a.second < b.second; })->first; +} + +template static std::vector angles_per_layer(const Print &print, RolePred role_wanted) +{ + std::vector angles; + for (const Layer *layer : print.objects().front()->layers()) + angles.push_back(dominant_fill_angle(*layer, role_wanted)); + return angles; +} + +static bool solid_role(ExtrusionRole role) { return is_solid_infill(role) && role != erIroning; } +static bool sparse_role(ExtrusionRole role) { return role == erInternalInfill; } +static bool ironing_role(ExtrusionRole role) { return role == erIroning; } + +TEST_CASE("Infill rotation template is unaffected by a raft", "[Fill][Regression]") +{ + // More angles than raft layers, so a raft cannot alias back to the same angle. + const std::string template_string = GENERATE("+45", "0,25,50,75,100,125,150"); + const int raft_layers = GENERATE(1, 3); + CAPTURE(template_string, raft_layers); + + auto angles_for = [&template_string](int rafts) { + Print print; + // 100% density makes every layer solid, so the template shows on all 100, not just shells. + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, + {{"solid_infill_rotate_template", template_string}, + {"sparse_infill_density", "100%"}, + {"internal_solid_infill_pattern", "monotonic"}, + {"layer_height", 0.2}, + {"raft_layers", rafts}}); + return angles_per_layer(print, solid_role); + }; + + const std::vector without_raft = angles_for(0); + const std::vector with_raft = angles_for(raft_layers); + + REQUIRE(without_raft.size() == 100); + REQUIRE(with_raft.size() == without_raft.size()); + REQUIRE(std::count(without_raft.begin(), without_raft.end(), -1) == 0); + CHECK(with_raft == without_raft); +} + +TEST_CASE("Sparse infill rotation template turns the infill layer by layer", "[Fill]") +{ + const std::vector expected_cycle = {0, 25, 50, 75, 100, 125, 150}; + + Print print; + // No shells, so every layer is sparse infill rather than solid. + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print, + {{"sparse_infill_rotate_template", "0,25,50,75,100,125,150"}, + {"sparse_infill_density", "40%"}, + {"sparse_infill_pattern", "rectilinear"}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"layer_height", 0.2}}); + + const std::vector angles = angles_per_layer(print, sparse_role); + REQUIRE(angles.size() == 50); + REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0); + + std::vector expected; + for (size_t i = 0; i < angles.size(); ++i) + expected.push_back(expected_cycle[i % expected_cycle.size()]); + CHECK(angles == expected); +} + +TEST_CASE("Infill rotation template layer count modifier holds each angle for N layers", "[Fill]") +{ + Print print; + // "+45#2" turns 45 degrees every 2 layers, so equal angles come in pairs. + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print, + {{"solid_infill_rotate_template", "+45#2"}, + {"sparse_infill_density", "100%"}, + {"internal_solid_infill_pattern", "monotonic"}, + {"layer_height", 0.2}}); + + const std::vector angles = angles_per_layer(print, solid_role); + REQUIRE(angles.size() == 50); + REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0); + + std::vector run_lengths; + for (size_t i = 0; i < angles.size();) { + size_t j = i; + while (j < angles.size() && angles[j] == angles[i]) + ++j; + run_lengths.push_back(int(j - i)); + i = j; + } + // The first and last runs can be clipped by the start and end of the object. + REQUIRE(run_lengths.size() > 3); + const std::vector interior(run_lengths.begin() + 1, run_lengths.end() - 1); + CHECK(std::count(interior.begin(), interior.end(), 2) == int(interior.size())); +} + +TEST_CASE("Z anti-aliasing keeps the infill rotation template's step", "[Fill]") +{ + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print, + {{"solid_infill_rotate_template", "+45"}, + {"sparse_infill_density", "100%"}, + {"internal_solid_infill_pattern", "monotonic"}, + {"zaa_enabled", 1}, + {"zaa_min_z", 0.05}, + {"layer_height", 0.2}}); + + // Z contouring varies the layer heights, so the layer count is not 10mm / 0.2mm here. + const std::vector angles = angles_per_layer(print, solid_role); + REQUIRE(angles.size() > 10); + REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0); + + // Z contouring may change when the template advances, but each step must still be 45 degrees. + int steps = 0; + for (size_t i = 1; i < angles.size(); ++i) { + const int delta = ((angles[i] - angles[i - 1]) % 180 + 180) % 180; + CAPTURE(i, angles[i - 1], angles[i]); + // Split rather than "delta == 0 || delta == 45" so Catch2 can show the operands. + REQUIRE(delta % 45 == 0); + REQUIRE(delta <= 45); + steps += delta == 45; + } + CHECK(steps > 0); +} + +TEST_CASE("Ironing follows the solid infill rotation template", "[Fill]") +{ + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print, + {{"solid_infill_rotate_template", "+45"}, + {"internal_solid_infill_pattern", "monotonic"}, + {"top_surface_pattern", "monotonic"}, + // Every solid surface, so the comparison covers every layer. + {"ironing_type", "solid"}, + {"sparse_infill_density", "100%"}, + {"ironing_angle", 0}, + {"ironing_angle_fixed", 0}, + {"layer_height", 0.2}}); + + const std::vector ironing = angles_per_layer(print, ironing_role); + const std::vector solid = angles_per_layer(print, solid_role); + REQUIRE(ironing.size() == solid.size()); + + // With no fixed angle and no offset, ironing runs along the template's angle for that layer. + int compared = 0; + for (size_t i = 0; i < ironing.size(); ++i) + if (ironing[i] != -1 && solid[i] != -1) { + CAPTURE(i, ironing[i], solid[i]); + CHECK(ironing[i] == solid[i]); + ++compared; + } + // Most of the object, not one lucky layer. + REQUIRE(compared > int(ironing.size()) / 2); +} + +TEST_CASE("Solid infill direction offsets every layer when no template is set", "[Fill]") +{ + auto angles_for = [](int direction) { + Print print; + Slic3r::Test::init_and_process_print({Slic3r::Test::cube(10)}, print, + {{"solid_infill_direction", direction}, + {"sparse_infill_density", "100%"}, + {"internal_solid_infill_pattern", "monotonic"}, + {"layer_height", 0.2}}); + return angles_per_layer(print, solid_role); + }; + + const std::vector at_0 = angles_for(0); + const std::vector at_30 = angles_for(30); + REQUIRE(at_0.size() == at_30.size()); + REQUIRE(std::count(at_0.begin(), at_0.end(), -1) == 0); + + for (size_t i = 0; i < at_0.size(); ++i) { + const int delta = ((at_30[i] - at_0[i]) % 180 + 180) % 180; + CAPTURE(i, at_0[i], at_30[i]); + CHECK(delta == 30); + } +}