mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-23 02:42:08 +00:00
fix: infill rotation template crashes and misbehaves on raft prints (#14894)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user