fix: infill rotation template crashes and misbehaves on raft prints (#14894)

This commit is contained in:
Kris Austin
2026-07-22 15:51:55 -05:00
committed by GitHub
parent b8cfa32a41
commit f23ad64303
2 changed files with 229 additions and 1 deletions

View File

@@ -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;

View File

@@ -1,12 +1,18 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <cmath>
#include <map>
#include <numeric>
#include <sstream>
#include <string>
#include <vector>
#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<typename RolePred> static int dominant_fill_angle(const Layer &layer, RolePred role_wanted)
{
std::map<int, double> 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<const ExtrusionPath *>(entity))
account(*path);
else if (auto *multi = dynamic_cast<const ExtrusionMultiPath *>(entity))
for (const ExtrusionPath &p : multi->paths)
account(p);
else if (auto *loop = dynamic_cast<const ExtrusionLoop *>(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<typename RolePred> static std::vector<int> angles_per_layer(const Print &print, RolePred role_wanted)
{
std::vector<int> 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<int> without_raft = angles_for(0);
const std::vector<int> 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<int> 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<int> angles = angles_per_layer(print, sparse_role);
REQUIRE(angles.size() == 50);
REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0);
std::vector<int> 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<int> angles = angles_per_layer(print, solid_role);
REQUIRE(angles.size() == 50);
REQUIRE(std::count(angles.begin(), angles.end(), -1) == 0);
std::vector<int> 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<int> 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<int> 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<int> ironing = angles_per_layer(print, ironing_role);
const std::vector<int> 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<int> at_0 = angles_for(0);
const std::vector<int> 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);
}
}