mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 02:37:04 +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()) {
|
if (template_string.empty()) {
|
||||||
return Geometry::deg2rad(fixed_infill_angle);
|
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;
|
double angle = 0.0;
|
||||||
ConfigOptionFloats rotate_angles;
|
ConfigOptionFloats rotate_angles;
|
||||||
const std::string search_string = "/NnZz$LlUuQq~^|#";
|
const std::string search_string = "/NnZz$LlUuQq~^|#";
|
||||||
@@ -75,6 +78,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
|||||||
double angle_start = 0;
|
double angle_start = 0;
|
||||||
double limit_fill_z = object->get_layer(0)->bottom_z();
|
double limit_fill_z = object->get_layer(0)->bottom_z();
|
||||||
double start_fill_z = limit_fill_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;
|
bool _noop = false;
|
||||||
auto fill_form = std::string::npos;
|
auto fill_form = std::string::npos;
|
||||||
bool _absolute = false;
|
bool _absolute = false;
|
||||||
@@ -84,7 +89,8 @@ double calculate_infill_rotation_angle(const PrintObject* object,
|
|||||||
for (int i = 0; i <= layer_id; i++) {
|
for (int i = 0; i <= layer_id; i++) {
|
||||||
double fill_z = object->get_layer(i)->bottom_z();
|
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
|
if (repeats) { // if repeats >0 then restore parameters for new iteration
|
||||||
limit_fill_z += limit_fill_z - start_fill_z;
|
limit_fill_z += limit_fill_z - start_fill_z;
|
||||||
start_fill_z = fill_z;
|
start_fill_z = fill_z;
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
#include <catch2/catch_all.hpp>
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <map>
|
||||||
#include <numeric>
|
#include <numeric>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "libslic3r/ClipperUtils.hpp"
|
#include "libslic3r/ClipperUtils.hpp"
|
||||||
#include "libslic3r/Fill/Fill.hpp"
|
#include "libslic3r/Fill/Fill.hpp"
|
||||||
#include "libslic3r/Flow.hpp"
|
#include "libslic3r/Flow.hpp"
|
||||||
#include "libslic3r/Geometry.hpp"
|
#include "libslic3r/Geometry.hpp"
|
||||||
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Print.hpp"
|
#include "libslic3r/Print.hpp"
|
||||||
#include "libslic3r/SVG.hpp"
|
#include "libslic3r/SVG.hpp"
|
||||||
#include "libslic3r/libslic3r.h"
|
#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
|
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