mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 22:31:02 +00:00
An ironing line spacing of 0 reached the fillers from a 3MF, the CLI or the per-filament override, which has no GUI guard. Concentric ironing then never finished slicing, because a zero inset never shrinks the region, and rectilinear ironing was silently dropped. Tiny positive values produced an unprintable number of lines. Top surface and support ironing now clamp the spacing to the 0.05 mm floor the process GUI guard already enforces, so these configurations iron at that spacing. Spacings at or above the floor, including every shipped profile, are unchanged. The concentric filler also returns early on a non-positive step so no other caller can hang it, and the filament settings page now resets a too-small override the same way the process page does.
1763 lines
83 KiB
C++
1763 lines
83 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <functional>
|
|
#include <map>
|
|
#include <numeric>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "libslic3r/ClipperUtils.hpp"
|
|
#include "libslic3r/AABBTreeLines.hpp"
|
|
#include "libslic3r/Fill/Fill.hpp"
|
|
#include "libslic3r/Fill/FillAdaptive.hpp"
|
|
#include "libslic3r/Fill/FillGyroid.hpp"
|
|
#include "libslic3r/Flow.hpp"
|
|
#include "libslic3r/Geometry.hpp"
|
|
#include "libslic3r/IntersectionPoints.hpp"
|
|
#include "libslic3r/Layer.hpp"
|
|
#include "libslic3r/Print.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "libslic3r/SVG.hpp"
|
|
#include "libslic3r/libslic3r.h"
|
|
|
|
#include "test_helpers.hpp"
|
|
|
|
using namespace Slic3r;
|
|
|
|
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
|
|
|
|
#if 0
|
|
TEST_CASE("Adjusted solid distance", "[Fill]") {
|
|
int surface_width = 250;
|
|
int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
|
|
REQUIRE(distance == Catch::Approx(50));
|
|
REQUIRE(surface_width % distance == 0);
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE("Pattern path length", "[Fill]") {
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
|
|
filler->angle = float(-(PI)/2.0);
|
|
FillParams fill_params;
|
|
filler->spacing = 5;
|
|
fill_params.dont_adjust = true;
|
|
//fill_params.endpoints_overlap = false;
|
|
fill_params.density = float(filler->spacing / 50.0);
|
|
|
|
auto test = [&filler, &fill_params] (const ExPolygon& poly) -> Slic3r::Polylines {
|
|
Slic3r::Surface surface(stTop, poly);
|
|
return filler->fill_surface(&surface, fill_params);
|
|
};
|
|
|
|
SECTION("Square") {
|
|
Slic3r::Points test_set;
|
|
test_set.reserve(4);
|
|
std::vector<Vec2d> points {Vec2d(0,0), Vec2d(100,0), Vec2d(100,100), Vec2d(0,100)};
|
|
for (size_t i = 0; i < 4; ++i) {
|
|
std::transform(points.cbegin()+i, points.cend(), std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
|
|
std::transform(points.cbegin(), points.cbegin()+i, std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
|
|
Slic3r::Polylines paths = test(Slic3r::ExPolygon(test_set));
|
|
REQUIRE(paths.size() == 1); // one continuous path
|
|
|
|
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
|
|
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
|
|
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
|
|
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
|
|
|
|
test_set.clear();
|
|
}
|
|
}
|
|
SECTION("Diamond with endpoints on grid") {
|
|
std::vector<Vec2d> points {Vec2d(0,0), Vec2d(100,0), Vec2d(150,50), Vec2d(100,100), Vec2d(0,100), Vec2d(-50,50)};
|
|
Slic3r::Points test_set;
|
|
test_set.reserve(6);
|
|
std::transform(points.cbegin(), points.cend(), std::back_inserter(test_set), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
|
|
Slic3r::Polylines paths = test(Slic3r::ExPolygon(test_set));
|
|
REQUIRE(paths.size() == 1); // one continuous path
|
|
}
|
|
|
|
SECTION("Square with hole") {
|
|
std::vector<Vec2d> square {Vec2d(0,0), Vec2d(100,0), Vec2d(100,100), Vec2d(0,100)};
|
|
std::vector<Vec2d> hole {Vec2d(25,25), Vec2d(75,25), Vec2d(75,75), Vec2d(25,75) };
|
|
std::reverse(hole.begin(), hole.end());
|
|
|
|
Slic3r::Points test_hole;
|
|
Slic3r::Points test_square;
|
|
|
|
std::transform(square.cbegin(), square.cend(), std::back_inserter(test_square), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
|
|
std::transform(hole.cbegin(), hole.cend(), std::back_inserter(test_hole), [] (const Vec2d& a) -> Point { return Point::new_scale(a.x(), a.y()); } );
|
|
|
|
for (double angle : {-(PI/2.0), -(PI/4.0), -(PI), PI/2.0, PI}) {
|
|
for (double spacing : {25.0, 5.0, 7.5, 8.5}) {
|
|
fill_params.density = float(filler->spacing / spacing);
|
|
filler->angle = float(angle);
|
|
ExPolygon e(test_square, test_hole);
|
|
Slic3r::Polylines paths = test(e);
|
|
#if 0
|
|
{
|
|
BoundingBox bbox = get_extents(e);
|
|
SVG svg("c:\\data\\temp\\square_with_holes.svg", bbox);
|
|
svg.draw(e);
|
|
svg.draw(paths);
|
|
svg.Close();
|
|
}
|
|
#endif
|
|
REQUIRE((paths.size() >= 1 && paths.size() <= 3));
|
|
// paths don't cross hole
|
|
REQUIRE(diff_pl(paths, offset(e, float(SCALED_EPSILON*10))).size() == 0);
|
|
}
|
|
}
|
|
}
|
|
SECTION("Regression: Missing infill segments in some rare circumstances") {
|
|
filler->angle = float(PI/4.0);
|
|
fill_params.dont_adjust = false;
|
|
filler->spacing = 0.654498;
|
|
//filler->endpoints_overlap = unscale(359974);
|
|
fill_params.density = 1;
|
|
filler->layer_id = 66;
|
|
filler->z = 20.15;
|
|
|
|
Slic3r::Points points {Point(25771516,14142125),Point(14142138,25771515),Point(2512749,14142131),Point(14142125,2512749)};
|
|
Slic3r::Polylines paths = test(Slic3r::ExPolygon(points));
|
|
REQUIRE(paths.size() == 1); // one continuous path
|
|
|
|
// TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon.
|
|
// This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon.
|
|
// ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length';
|
|
REQUIRE(std::abs(paths[0].length() - static_cast<double>(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length
|
|
}
|
|
|
|
SECTION("Rotated Square") {
|
|
Slic3r::Points square { Point::new_scale(0,0), Point::new_scale(50,0), Point::new_scale(50,50), Point::new_scale(0,50)};
|
|
Slic3r::ExPolygon expolygon(square);
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
|
|
filler->bounding_box = get_extents(expolygon.contour);
|
|
filler->angle = 0;
|
|
|
|
Surface surface(stTop, expolygon);
|
|
auto flow = Slic3r::Flow(0.69f, 0.4f, 0.50f);
|
|
|
|
FillParams fill_params;
|
|
fill_params.density = 1.0;
|
|
filler->spacing = flow.spacing();
|
|
|
|
for (auto angle : { 0.0, 45.0}) {
|
|
surface.expolygon.rotate(angle, Point(0,0));
|
|
Polylines paths = filler->fill_surface(&surface, fill_params);
|
|
REQUIRE(paths.size() == 1);
|
|
}
|
|
}
|
|
|
|
#if 0 // Disabled temporarily due to precision issues on the Mac VM
|
|
SECTION("Solid surface fill") {
|
|
Slic3r::Points points {
|
|
Point::new_scale(6883102, 9598327.01296997),
|
|
Point::new_scale(6883102, 20327272.01297),
|
|
Point::new_scale(3116896, 20327272.01297),
|
|
Point::new_scale(3116896, 9598327.01296997)
|
|
};
|
|
Slic3r::ExPolygon expolygon(points);
|
|
|
|
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
|
|
for (size_t i = 0; i <= 20; ++i)
|
|
{
|
|
expolygon.scale(1.05);
|
|
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
SECTION("Solid surface fill") {
|
|
Slic3r::Points points {
|
|
Slic3r::Point(59515297,5422499),Slic3r::Point(59531249,5578697),Slic3r::Point(59695801,6123186),
|
|
Slic3r::Point(59965713,6630228),Slic3r::Point(60328214,7070685),Slic3r::Point(60773285,7434379),
|
|
Slic3r::Point(61274561,7702115),Slic3r::Point(61819378,7866770),Slic3r::Point(62390306,7924789),
|
|
Slic3r::Point(62958700,7866744),Slic3r::Point(63503012,7702244),Slic3r::Point(64007365,7434357),
|
|
Slic3r::Point(64449960,7070398),Slic3r::Point(64809327,6634999),Slic3r::Point(65082143,6123325),
|
|
Slic3r::Point(65245005,5584454),Slic3r::Point(65266967,5422499),Slic3r::Point(66267307,5422499),
|
|
Slic3r::Point(66269190,8310081),Slic3r::Point(66275379,17810072),Slic3r::Point(66277259,20697500),
|
|
Slic3r::Point(65267237,20697500),Slic3r::Point(65245004,20533538),Slic3r::Point(65082082,19994444),
|
|
Slic3r::Point(64811462,19488579),Slic3r::Point(64450624,19048208),Slic3r::Point(64012101,18686514),
|
|
Slic3r::Point(63503122,18415781),Slic3r::Point(62959151,18251378),Slic3r::Point(62453416,18198442),
|
|
Slic3r::Point(62390147,18197355),Slic3r::Point(62200087,18200576),Slic3r::Point(61813519,18252990),
|
|
Slic3r::Point(61274433,18415918),Slic3r::Point(60768598,18686517),Slic3r::Point(60327567,19047892),
|
|
Slic3r::Point(59963609,19493297),Slic3r::Point(59695865,19994587),Slic3r::Point(59531222,20539379),
|
|
Slic3r::Point(59515153,20697500),Slic3r::Point(58502480,20697500),Slic3r::Point(58502480,5422499)
|
|
};
|
|
Slic3r::ExPolygon expolygon(points);
|
|
|
|
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55) == true);
|
|
REQUIRE(test_if_solid_surface_filled(expolygon, 0.55, PI/2.0) == true);
|
|
}
|
|
SECTION("Solid surface fill") {
|
|
Slic3r::Points points {
|
|
Point::new_scale(0,0),Point::new_scale(98,0),Point::new_scale(98,10), Point::new_scale(0,10)
|
|
};
|
|
Slic3r::ExPolygon expolygon(points);
|
|
|
|
REQUIRE(test_if_solid_surface_filled(expolygon, 0.5, 45.0, 0.99) == true);
|
|
}
|
|
}
|
|
|
|
/*
|
|
{
|
|
my $collection = Slic3r::Polyline::Collection->new(
|
|
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
|
|
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
|
|
);
|
|
is_deeply
|
|
[ map $_->[Y], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
|
|
[20, 18, 15, 10, 8, 5],
|
|
'chained path';
|
|
}
|
|
|
|
{
|
|
my $collection = Slic3r::Polyline::Collection->new(
|
|
Slic3r::Polyline->new([4,0], [10,0], [15,0]),
|
|
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
|
|
);
|
|
is_deeply
|
|
[ map $_->[X], map @$_, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
|
|
[reverse 4, 10, 15, 10, 15, 20],
|
|
'chained path';
|
|
}
|
|
|
|
{
|
|
my $collection = Slic3r::ExtrusionPath::Collection->new(
|
|
map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
|
|
Slic3r::Polyline->new([0,15], [0,18], [0,20]),
|
|
Slic3r::Polyline->new([0,10], [0,8], [0,5]),
|
|
);
|
|
is_deeply
|
|
[ map $_->[Y], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(0,30), 0)} ],
|
|
[20, 18, 15, 10, 8, 5],
|
|
'chained path';
|
|
}
|
|
|
|
{
|
|
my $collection = Slic3r::ExtrusionPath::Collection->new(
|
|
map Slic3r::ExtrusionPath->new(polyline => $_, role => 0, mm3_per_mm => 1),
|
|
Slic3r::Polyline->new([15,0], [10,0], [4,0]),
|
|
Slic3r::Polyline->new([10,5], [15,5], [20,5]),
|
|
);
|
|
is_deeply
|
|
[ map $_->[X], map @{$_->polyline}, @{$collection->chained_path_from(Slic3r::Point->new(30,0), 0)} ],
|
|
[reverse 4, 10, 15, 10, 15, 20],
|
|
'chained path';
|
|
}
|
|
|
|
for my $pattern (qw(rectilinear honeycomb hilbertcurve concentric)) {
|
|
my $config = Slic3r::Config->new_from_defaults;
|
|
$config->set('fill_pattern', $pattern);
|
|
$config->set('external_fill_pattern', $pattern);
|
|
$config->set('perimeters', 1);
|
|
$config->set('skirts', 0);
|
|
$config->set('fill_density', 20);
|
|
$config->set('layer_height', 0.05);
|
|
$config->set('perimeter_extruder', 1);
|
|
$config->set('infill_extruder', 2);
|
|
my $print = Slic3r::Test::init_print('20mm_cube', config => $config, scale => 2);
|
|
ok my $gcode = Slic3r::Test::gcode($print), "successful $pattern infill generation";
|
|
my $tool = undef;
|
|
my @perimeter_points = my @infill_points = ();
|
|
Slic3r::GCode::Reader->new->parse($gcode, sub {
|
|
my ($self, $cmd, $args, $info) = @_;
|
|
|
|
if ($cmd =~ /^T(\d+)/) {
|
|
$tool = $1;
|
|
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
|
|
if ($tool == $config->perimeter_extruder-1) {
|
|
push @perimeter_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
|
|
} elsif ($tool == $config->infill_extruder-1) {
|
|
push @infill_points, Slic3r::Point->new_scale($args->{X}, $args->{Y});
|
|
}
|
|
}
|
|
});
|
|
my $convex_hull = convex_hull(\@perimeter_points);
|
|
ok !(defined first { !$convex_hull->contains_point($_) } @infill_points), "infill does not exceed perimeters ($pattern)";
|
|
}
|
|
|
|
{
|
|
my $config = Slic3r::Config->new_from_defaults;
|
|
$config->set('infill_only_where_needed', 1);
|
|
$config->set('bottom_solid_layers', 0);
|
|
$config->set('infill_extruder', 2);
|
|
$config->set('infill_extrusion_width', 0.5);
|
|
$config->set('fill_density', 40);
|
|
$config->set('cooling', 0); # for preventing speeds from being altered
|
|
$config->set('first_layer_speed', '100%'); # for preventing speeds from being altered
|
|
|
|
my $test = sub {
|
|
my $print = Slic3r::Test::init_print('pyramid', config => $config);
|
|
|
|
my $tool = undef;
|
|
my @infill_extrusions = (); # array of polylines
|
|
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
|
|
my ($self, $cmd, $args, $info) = @_;
|
|
|
|
if ($cmd =~ /^T(\d+)/) {
|
|
$tool = $1;
|
|
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
|
|
if ($tool == $config->infill_extruder-1) {
|
|
push @infill_extrusions, Slic3r::Line->new_scale(
|
|
[ $self->X, $self->Y ],
|
|
[ $info->{new_X}, $info->{new_Y} ],
|
|
);
|
|
}
|
|
}
|
|
});
|
|
return 0 if !@infill_extrusions; # prevent calling convex_hull() with no points
|
|
|
|
my $convex_hull = convex_hull([ map $_->pp, map @$_, @infill_extrusions ]);
|
|
return unscale unscale sum(map $_->area, @{offset([$convex_hull], scale(+$config->infill_extrusion_width/2))});
|
|
};
|
|
|
|
my $tolerance = 5; # mm^2
|
|
|
|
$config->set('solid_infill_below_area', 0);
|
|
ok $test->() < $tolerance,
|
|
'no infill is generated when using infill_only_where_needed on a pyramid';
|
|
|
|
$config->set('solid_infill_below_area', 70);
|
|
ok abs($test->() - $config->solid_infill_below_area) < $tolerance,
|
|
'infill is only generated under the forced solid shells';
|
|
}
|
|
|
|
{
|
|
my $config = Slic3r::Config->new_from_defaults;
|
|
$config->set('skirts', 0);
|
|
$config->set('perimeters', 1);
|
|
$config->set('fill_density', 0);
|
|
$config->set('top_solid_layers', 0);
|
|
$config->set('bottom_solid_layers', 0);
|
|
$config->set('solid_infill_below_area', 20000000);
|
|
$config->set('solid_infill_every_layers', 2);
|
|
$config->set('perimeter_speed', 99);
|
|
$config->set('external_perimeter_speed', 99);
|
|
$config->set('cooling', 0);
|
|
$config->set('first_layer_speed', '100%');
|
|
|
|
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
|
|
my %layers_with_extrusion = ();
|
|
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
|
|
my ($self, $cmd, $args, $info) = @_;
|
|
|
|
if ($cmd eq 'G1' && $info->{dist_XY} > 0 && $info->{extruding}) {
|
|
if (($args->{F} // $self->F) != $config->perimeter_speed*60) {
|
|
$layers_with_extrusion{$self->Z} = ($args->{F} // $self->F);
|
|
}
|
|
}
|
|
});
|
|
|
|
ok !%layers_with_extrusion,
|
|
"solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0";
|
|
}
|
|
|
|
{
|
|
my $config = Slic3r::Config->new_from_defaults;
|
|
$config->set('skirts', 0);
|
|
$config->set('perimeters', 3);
|
|
$config->set('fill_density', 0);
|
|
$config->set('layer_height', 0.2);
|
|
$config->set('first_layer_height', 0.2);
|
|
$config->set('nozzle_diameter', [0.35]);
|
|
$config->set('infill_extruder', 2);
|
|
$config->set('solid_infill_extruder', 2);
|
|
$config->set('infill_extrusion_width', 0.52);
|
|
$config->set('solid_infill_extrusion_width', 0.52);
|
|
$config->set('first_layer_extrusion_width', 0);
|
|
|
|
my $print = Slic3r::Test::init_print('A', config => $config);
|
|
my %infill = (); # Z => [ Line, Line ... ]
|
|
my $tool = undef;
|
|
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
|
|
my ($self, $cmd, $args, $info) = @_;
|
|
|
|
if ($cmd =~ /^T(\d+)/) {
|
|
$tool = $1;
|
|
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
|
|
if ($tool == $config->infill_extruder-1) {
|
|
my $z = 1 * $self->Z;
|
|
$infill{$z} ||= [];
|
|
push @{$infill{$z}}, Slic3r::Line->new_scale(
|
|
[ $self->X, $self->Y ],
|
|
[ $info->{new_X}, $info->{new_Y} ],
|
|
);
|
|
}
|
|
}
|
|
});
|
|
my $grow_d = scale($config->infill_extrusion_width)/2;
|
|
my $layer0_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.2} } ]);
|
|
my $layer1_infill = union([ map @{$_->grow($grow_d)}, @{ $infill{0.4} } ]);
|
|
my $diff = diff($layer0_infill, $layer1_infill);
|
|
$diff = offset2_ex($diff, -$grow_d, +$grow_d);
|
|
$diff = [ grep { $_->area > 2*(($grow_d*2)**2) } @$diff ];
|
|
is scalar(@$diff), 0, 'no missing parts in solid shell when fill_density is 0';
|
|
}
|
|
|
|
{
|
|
# GH: #2697
|
|
my $config = Slic3r::Config->new_from_defaults;
|
|
$config->set('perimeter_extrusion_width', 0.72);
|
|
$config->set('top_infill_extrusion_width', 0.1);
|
|
$config->set('infill_extruder', 2); # in order to distinguish infill
|
|
$config->set('solid_infill_extruder', 2); # in order to distinguish infill
|
|
|
|
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
|
|
my %infill = (); # Z => [ Line, Line ... ]
|
|
my %other = (); # Z => [ Line, Line ... ]
|
|
my $tool = undef;
|
|
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
|
|
my ($self, $cmd, $args, $info) = @_;
|
|
|
|
if ($cmd =~ /^T(\d+)/) {
|
|
$tool = $1;
|
|
} elsif ($cmd eq 'G1' && $info->{extruding} && $info->{dist_XY} > 0) {
|
|
my $z = 1 * $self->Z;
|
|
my $line = Slic3r::Line->new_scale(
|
|
[ $self->X, $self->Y ],
|
|
[ $info->{new_X}, $info->{new_Y} ],
|
|
);
|
|
if ($tool == $config->infill_extruder-1) {
|
|
$infill{$z} //= [];
|
|
push @{$infill{$z}}, $line;
|
|
} else {
|
|
$other{$z} //= [];
|
|
push @{$other{$z}}, $line;
|
|
}
|
|
}
|
|
});
|
|
my $top_z = max(keys %infill);
|
|
my $top_infill_grow_d = scale($config->top_infill_extrusion_width)/2;
|
|
my $top_infill = union([ map @{$_->grow($top_infill_grow_d)}, @{ $infill{$top_z} } ]);
|
|
my $perimeters_grow_d = scale($config->perimeter_extrusion_width)/2;
|
|
my $perimeters = union([ map @{$_->grow($perimeters_grow_d)}, @{ $other{$top_z} } ]);
|
|
my $covered = union_ex([ @$top_infill, @$perimeters ]);
|
|
my @holes = map @{$_->holes}, @$covered;
|
|
ok sum(map unscale unscale $_->area*-1, @holes) < 1, 'no gaps between top solid infill and perimeters';
|
|
}
|
|
*/
|
|
|
|
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle, double density)
|
|
{
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
|
|
filler->bounding_box = get_extents(expolygon.contour);
|
|
filler->angle = float(angle);
|
|
|
|
Flow flow(float(flow_spacing), 0.4f, float(flow_spacing));
|
|
filler->spacing = flow.spacing();
|
|
|
|
FillParams fill_params;
|
|
fill_params.density = float(density);
|
|
fill_params.dont_adjust = false;
|
|
|
|
Surface surface(stBottom, expolygon);
|
|
Slic3r::Polylines paths = filler->fill_surface(&surface, fill_params);
|
|
|
|
// check whether any part was left uncovered
|
|
Polygons grown_paths;
|
|
grown_paths.reserve(paths.size());
|
|
|
|
// figure out what is actually going on here re: data types
|
|
float line_offset = float(scale_(filler->spacing / 2.0 + EPSILON));
|
|
std::for_each(paths.begin(), paths.end(), [line_offset, &grown_paths] (const Slic3r::Polyline& p) {
|
|
polygons_append(grown_paths, offset(p, line_offset));
|
|
});
|
|
|
|
// Shrink the initial expolygon a bit, this simulates the infill / perimeter overlap that we usually apply.
|
|
ExPolygons uncovered = diff_ex(offset(expolygon, - float(0.2 * scale_(flow_spacing))), grown_paths, ApplySafetyOffset::Yes);
|
|
|
|
// ignore very small dots
|
|
const double scaled_flow_spacing = std::pow(scale_(flow_spacing), 2);
|
|
uncovered.erase(std::remove_if(uncovered.begin(), uncovered.end(), [scaled_flow_spacing](const ExPolygon& poly) { return poly.area() < scaled_flow_spacing; }), uncovered.end());
|
|
|
|
#if 0
|
|
if (! uncovered.empty()) {
|
|
BoundingBox bbox = get_extents(expolygon.contour);
|
|
bbox.merge(get_extents(uncovered));
|
|
bbox.merge(get_extents(grown_paths));
|
|
SVG svg("c:\\data\\temp\\test_if_solid_surface_filled.svg", bbox);
|
|
svg.draw(expolygon);
|
|
svg.draw(uncovered, "red");
|
|
svg.Close();
|
|
}
|
|
#endif
|
|
|
|
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);
|
|
}
|
|
|
|
|
|
namespace {
|
|
|
|
PrintRegionConfig ironing_config(IroningType type,
|
|
int top_surface_filament_id = 1,
|
|
int top_shell_layers = 3,
|
|
int bottom_shell_layers = 1)
|
|
{
|
|
PrintRegionConfig cfg;
|
|
cfg.ironing_type.value = type;
|
|
cfg.top_surface_filament_id.value = top_surface_filament_id;
|
|
cfg.top_shell_layers.value = top_shell_layers;
|
|
cfg.bottom_shell_layers.value = bottom_shell_layers;
|
|
cfg.outer_wall_filament_id.value = 1;
|
|
cfg.wall_loops.value = 2;
|
|
return cfg;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Ironing an all-solid region uses the top surface filament on every layer", "[Fill]")
|
|
{
|
|
const PrintRegionConfig cfg = ironing_config(IroningType::AllSolid, /*top_surface_filament_id=*/2);
|
|
const bool is_topmost_layer = GENERATE(false, true);
|
|
CAPTURE(is_topmost_layer);
|
|
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, is_topmost_layer) == 2);
|
|
}
|
|
|
|
TEST_CASE("Ironing top surfaces uses the top surface filament when the region has top shells", "[Fill]")
|
|
{
|
|
const PrintRegionConfig cfg = ironing_config(IroningType::TopSurfaces,
|
|
/*top_surface_filament_id=*/3,
|
|
/*top_shell_layers=*/2);
|
|
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == 3);
|
|
}
|
|
|
|
TEST_CASE("Ironing top surfaces without top shells needs spiral mode and more than one bottom shell", "[Fill]")
|
|
{
|
|
const PrintRegionConfig one_bottom_shell = ironing_config(IroningType::TopSurfaces,
|
|
/*top_surface_filament_id=*/1,
|
|
/*top_shell_layers=*/0,
|
|
/*bottom_shell_layers=*/1);
|
|
const PrintRegionConfig two_bottom_shells = ironing_config(IroningType::TopSurfaces,
|
|
/*top_surface_filament_id=*/1,
|
|
/*top_shell_layers=*/0,
|
|
/*bottom_shell_layers=*/2);
|
|
|
|
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == 1);
|
|
REQUIRE(Layer::choose_ironing_extruder(one_bottom_shell, /*spiral_mode=*/true, /*is_topmost_layer=*/false) == -1);
|
|
REQUIRE(Layer::choose_ironing_extruder(two_bottom_shells, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
|
}
|
|
|
|
TEST_CASE("Ironing the topmost surface only applies to the topmost layer", "[Fill]")
|
|
{
|
|
const PrintRegionConfig cfg = ironing_config(IroningType::TopmostOnly, /*top_surface_filament_id=*/4);
|
|
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/true) == 4);
|
|
REQUIRE(Layer::choose_ironing_extruder(cfg, /*spiral_mode=*/false, /*is_topmost_layer=*/false) == -1);
|
|
}
|
|
|
|
TEST_CASE("A region with ironing turned off is never ironed", "[Fill]")
|
|
{
|
|
const PrintRegionConfig cfg = ironing_config(IroningType::NoIroning);
|
|
const bool spiral_mode = GENERATE(false, true);
|
|
CAPTURE(spiral_mode);
|
|
REQUIRE(Layer::choose_ironing_extruder(cfg, spiral_mode, /*is_topmost_layer=*/true) == -1);
|
|
}
|
|
|
|
// Ironing path count and total length in mm, over the whole object.
|
|
static std::pair<size_t, double> ironing_extent(const Print &print)
|
|
{
|
|
size_t paths = 0;
|
|
double length = 0.;
|
|
for (const Layer *layer : print.objects().front()->layers())
|
|
for (const LayerRegion *region : layer->regions())
|
|
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
|
if (ironing_role(entity->role())) {
|
|
++paths;
|
|
length += unscale<double>(entity->length());
|
|
}
|
|
return {paths, length};
|
|
}
|
|
|
|
TEST_CASE("Ironing spacing below the minimum irons at the minimum spacing", "[Fill]")
|
|
{
|
|
const std::string pattern = GENERATE("rectilinear", "concentric");
|
|
const bool via_filament = GENERATE(false, true);
|
|
const double spacing = GENERATE(0., 0.001);
|
|
CAPTURE(pattern, via_filament, spacing);
|
|
|
|
auto ironing_for = [&pattern, via_filament](double spacing) {
|
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({{"ironing_type", "top"},
|
|
{"ironing_pattern", pattern},
|
|
{"layer_height", 0.2}});
|
|
// The filament override replaces the process spacing, which stays at a usable value.
|
|
if (via_filament)
|
|
config.set_deserialize_strict({{"ironing_spacing", 0.1}, {"filament_ironing_spacing", spacing}});
|
|
else
|
|
config.set_deserialize_strict({{"ironing_spacing", spacing}});
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print, config);
|
|
return ironing_extent(print);
|
|
};
|
|
|
|
const std::pair<size_t, double> clamped = ironing_for(spacing);
|
|
const std::pair<size_t, double> minimum = ironing_for(IRONING_SPACING_MIN);
|
|
REQUIRE(minimum.first > 0);
|
|
CHECK(clamped.first == minimum.first);
|
|
CHECK_THAT(clamped.second, Catch::Matchers::WithinRel(minimum.second, 1e-9));
|
|
}
|
|
|
|
TEST_CASE("Concentric fill at zero spacing returns without paths", "[Fill]")
|
|
{
|
|
std::unique_ptr<Fill> filler(Fill::new_from_type(ipConcentric));
|
|
filler->spacing = 0.;
|
|
filler->bounding_box = BoundingBox(Point(0, 0), Point::new_scale(10, 10));
|
|
FillParams params;
|
|
params.density = 1.f;
|
|
Surface surface(stTop, ExPolygon({Point(0, 0), Point::new_scale(10, 0), Point::new_scale(10, 10), Point::new_scale(0, 10)}));
|
|
CHECK(filler->fill_surface(&surface, params).empty());
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// Orca: the spiral inset pattern chains the concentric loops into a single continuous path per
|
|
// island, so it has to cope with the degenerate loops offsetting leaves behind and it must not join
|
|
// loops that only look adjacent.
|
|
namespace {
|
|
|
|
Slic3r::Polylines spiral_inset_fill(const Slic3r::ExPolygon &surface_shape, double spacing)
|
|
{
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("spiralinset"));
|
|
filler->spacing = spacing;
|
|
// Cancel the half-spacing contraction fill_surface() applies, so the filler sees the shape as given.
|
|
filler->overlap = 0.5 * spacing;
|
|
|
|
Slic3r::FillParams fill_params;
|
|
fill_params.density = 1.f;
|
|
fill_params.dont_adjust = true;
|
|
|
|
Slic3r::Surface surface(Slic3r::stBottom, surface_shape);
|
|
return filler->fill_surface(&surface, fill_params);
|
|
}
|
|
|
|
Slic3r::ExPolygon rectangle(double x, double y, double w, double h)
|
|
{
|
|
return Slic3r::ExPolygon({Slic3r::Point::new_scale(x, y), Slic3r::Point::new_scale(x + w, y),
|
|
Slic3r::Point::new_scale(x + w, y + h), Slic3r::Point::new_scale(x, y + h)});
|
|
}
|
|
|
|
// Area of the surface the toolpaths fail to cover, and the largest single patch of it, in mm2. Each
|
|
// bead is measured at its own width so the variable width walls are not sold short.
|
|
std::pair<double, double> uncovered_area(const Slic3r::ExPolygon &surface_shape, const Slic3r::Polygons &covered)
|
|
{
|
|
double total = 0, biggest = 0;
|
|
for (const Slic3r::ExPolygon &gap : Slic3r::diff_ex(Slic3r::ExPolygons{surface_shape}, Slic3r::union_(covered))) {
|
|
const double area = unscale<double>(unscale<double>(gap.area()));
|
|
total += area;
|
|
biggest = std::max(biggest, area);
|
|
}
|
|
return {total, biggest};
|
|
}
|
|
|
|
Slic3r::Polygons beads_of(const Slic3r::Polylines &paths, double width)
|
|
{
|
|
return Slic3r::offset(paths, float(scale_(0.5 * width)));
|
|
}
|
|
|
|
Slic3r::Polygons beads_of(const Slic3r::ThickPolylines &paths)
|
|
{
|
|
Slic3r::Polygons covered;
|
|
for (const Slic3r::ThickPolyline &path : paths)
|
|
for (size_t i = 0; i + 1 < path.points.size(); ++i) {
|
|
Slic3r::Polyline segment;
|
|
segment.points = {path.points[i], path.points[i + 1]};
|
|
Slic3r::append(covered, Slic3r::offset(Slic3r::Polylines{segment},
|
|
float(0.5 * std::max(path.width[2 * i], path.width[2 * i + 1]))));
|
|
}
|
|
return covered;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Spiral inset fill drops loops shorter than the end clipping", "[Fill][Regression]")
|
|
{
|
|
// A sliver whose whole perimeter is shorter than the length clipped off the end of a loop, so the
|
|
// clipping consumes the path entirely. Such a loop carries no extrusion and must be dropped
|
|
// rather than kept as an empty path and read back from.
|
|
const double spacing = 0.45;
|
|
|
|
Slic3r::Polylines paths;
|
|
REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 0.05, 0.05), spacing));
|
|
for (const Slic3r::Polyline &path : paths)
|
|
CHECK(path.size() >= 2);
|
|
|
|
// The same surface at a size the clipping cannot swallow still gets filled.
|
|
REQUIRE_NOTHROW(paths = spiral_inset_fill(rectangle(0, 0, 5, 5), spacing));
|
|
REQUIRE(paths.size() == 1);
|
|
CHECK(paths.front().size() >= 2);
|
|
}
|
|
|
|
TEST_CASE("Spiral inset fill keeps separate islands on separate paths", "[Fill]")
|
|
{
|
|
// Two lobes joined by a neck narrower than the loop spacing: the inward offsets break the surface
|
|
// into two islands, which cannot share one spiral, and no path may leave the surface.
|
|
const double spacing = 0.45;
|
|
Slic3r::ExPolygon dumbbell = rectangle(0, 0, 6, 6);
|
|
dumbbell = Slic3r::union_ex(Slic3r::ExPolygons{dumbbell, rectangle(6, 2.9, 4, 0.2), rectangle(10, 0, 6, 6)}).front();
|
|
|
|
const Slic3r::Polylines paths = spiral_inset_fill(dumbbell, spacing);
|
|
REQUIRE(paths.size() >= 2);
|
|
|
|
// Inflate by a hair so that loops sitting exactly on the outline still count as contained.
|
|
const Slic3r::ExPolygons within = Slic3r::offset_ex(dumbbell, float(SCALED_EPSILON));
|
|
REQUIRE(within.size() == 1);
|
|
for (const Slic3r::Polyline &path : paths) {
|
|
CHECK(path.size() >= 2);
|
|
CHECK(within.front().contains(path));
|
|
}
|
|
}
|
|
|
|
|
|
TEST_CASE("Spiral inset fill stays connected across sharp corners", "[Fill][Regression]")
|
|
{
|
|
// At a corner of half-angle a, the next ring inward retreats along the bisector by spacing/sin(a),
|
|
// which leaves it several spacings from the end of the ring it continues. Judging the break by
|
|
// distance broke the spiral into loose rings at every spike; nesting is what decides the island.
|
|
const double spacing = 0.45;
|
|
const Slic3r::ExPolygon spike({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(30, 0),
|
|
Slic3r::Point::new_scale(15, 4)});
|
|
|
|
const Slic3r::Polylines paths = spiral_inset_fill(spike, spacing);
|
|
CHECK(paths.size() == 1);
|
|
|
|
const Slic3r::ExPolygons within = Slic3r::offset_ex(spike, float(SCALED_EPSILON));
|
|
REQUIRE(within.size() == 1);
|
|
for (const Slic3r::Polyline &path : paths)
|
|
CHECK(within.front().contains(path));
|
|
}
|
|
|
|
TEST_CASE("Spiral inset fill starts on a convex corner", "[Fill][Regression]")
|
|
{
|
|
// The only right angle on this outline is the reflex one: the two edges meeting at the origin
|
|
// span 90 degrees exactly as a square corner would, but the material lies outside them. The next
|
|
// ring in steps away from a reflex corner along the bisector instead of hugging it, so starting
|
|
// the spiral there sent it across a long diagonal on every single ring.
|
|
const double spacing = 0.45;
|
|
const Slic3r::ExPolygon notched({Slic3r::Point::new_scale(0, 0), Slic3r::Point::new_scale(0, 10),
|
|
Slic3r::Point::new_scale(-16, 18), Slic3r::Point::new_scale(-16, -2),
|
|
Slic3r::Point::new_scale(-8, -16), Slic3r::Point::new_scale(18, -16),
|
|
Slic3r::Point::new_scale(10, 0)});
|
|
|
|
const Slic3r::Polylines paths = spiral_inset_fill(notched, spacing);
|
|
REQUIRE(paths.size() >= 1);
|
|
|
|
// Every edge of the outline is at least 45 degrees off the bisector of that reflex corner, and
|
|
// so is every ring offset from it. A long segment running along the bisector can therefore only
|
|
// be the spiral striking out across the rings to reach the next one.
|
|
for (const Slic3r::Polyline &path : paths)
|
|
for (const Slic3r::Line &segment : path.lines()) {
|
|
const Vec2d v = (segment.b - segment.a).cast<double>();
|
|
const double direction = std::fmod(std::atan2(v.y(), v.x()) * 180.0 / M_PI + 180.0, 180.0);
|
|
if (std::abs(direction - 45.0) > 25.0)
|
|
continue;
|
|
CAPTURE(direction, unscale<double>(segment.length()));
|
|
CHECK(segment.length() <= scale_(1.5 * spacing));
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Spiral inset fill closes the gaps with variable width walls", "[Fill]")
|
|
{
|
|
// Fixed width loops cannot fill a region that is not a whole number of lines across and leave the
|
|
// remainder open, which on a ring shows up as a wedge several lines wide. Plain concentric avoids
|
|
// that by building solid surfaces out of Arachne's variable width walls, and so must this pattern.
|
|
const double spacing = 0.45;
|
|
Slic3r::ExPolygon ring = rectangle(0, 0, 24, 24);
|
|
Slic3r::Polygon hole;
|
|
for (int i = 0; i < 64; ++i) {
|
|
const double angle = -2.0 * PI * i / 64.0; // clockwise, so it reads as a hole
|
|
hole.points.emplace_back(Slic3r::Point::new_scale(12 + 7.3 * std::cos(angle), 12 + 7.3 * std::sin(angle)));
|
|
}
|
|
ring.holes.emplace_back(hole);
|
|
|
|
Slic3r::PrintConfig print_config;
|
|
Slic3r::PrintObjectConfig object_config;
|
|
auto make_filler = [&]() {
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("spiralinset"));
|
|
filler->spacing = spacing;
|
|
filler->overlap = 0.5 * spacing; // cancel the contraction, so both see the same surface
|
|
filler->print_config = &print_config;
|
|
filler->print_object_config = &object_config;
|
|
return filler;
|
|
};
|
|
|
|
Slic3r::FillParams params;
|
|
params.density = 1.f;
|
|
params.dont_adjust = false;
|
|
params.layer_height = 0.2;
|
|
|
|
const Slic3r::Surface surface(Slic3r::stTop, ring);
|
|
|
|
std::unique_ptr<Slic3r::Fill> fixed = make_filler();
|
|
const Slic3r::Polylines fixed_width = fixed->fill_surface(&surface, params);
|
|
REQUIRE(!fixed_width.empty());
|
|
const auto fixed_gaps = uncovered_area(ring, beads_of(fixed_width, fixed->spacing));
|
|
|
|
params.use_arachne = true;
|
|
std::unique_ptr<Slic3r::Fill> variable = make_filler();
|
|
const Slic3r::ThickPolylines variable_width = variable->fill_surface_arachne(&surface, params);
|
|
REQUIRE(!variable_width.empty());
|
|
const auto variable_gaps = uncovered_area(ring, beads_of(variable_width));
|
|
|
|
CAPTURE(fixed_gaps.first, fixed_gaps.second, variable_gaps.first, variable_gaps.second);
|
|
// The wedges the fixed width loops leave behind are what the variable width walls take up.
|
|
CHECK(variable_gaps.second < 0.5 * fixed_gaps.second);
|
|
CHECK(variable_gaps.first < fixed_gaps.first);
|
|
|
|
// And it is still a spiral: far fewer paths than the ring has loops.
|
|
// And the walls are still chained into spirals rather than printed one path per wall. The ring is
|
|
// at its narrowest (12 - 7.3) mm across and is filled from both sides, so it is at least this many
|
|
// walls thick there and thicker elsewhere. Arachne's short thin feature walls cannot join a spiral,
|
|
// so only the substantial paths count towards this.
|
|
const size_t walls_across = size_t(2.0 * (12.0 - 7.3) / spacing);
|
|
size_t spirals = 0;
|
|
for (const Slic3r::ThickPolyline &path : variable_width)
|
|
if (path.length() > scale_(10.0 * spacing))
|
|
++spirals;
|
|
CAPTURE(spirals, walls_across, variable_width.size(), fixed_width.size());
|
|
CHECK(2 * spirals < walls_across);
|
|
}
|
|
|
|
TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]")
|
|
{
|
|
// A cell whose sides are several times the line width, so that the corners have room to be rounded.
|
|
const double spacing = 0.45;
|
|
const double density = 0.1;
|
|
auto fill = [spacing, density](double smooth_factor) {
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("honeycomb"));
|
|
filler->spacing = spacing;
|
|
|
|
FillParams params;
|
|
params.density = float(density);
|
|
params.dont_adjust = true;
|
|
// Keep the fragments apart, so that only the turns of the pattern itself are measured.
|
|
params.anchor_length_max = 0.f;
|
|
params.smooth_factor = smooth_factor;
|
|
|
|
Slic3r::ExPolygon square{ Slic3r::Points{
|
|
Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } };
|
|
Slic3r::Surface surface(stInternal, square);
|
|
return filler->fill_surface(&surface, params);
|
|
};
|
|
|
|
// Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all.
|
|
auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) {
|
|
double sharpest = 1.;
|
|
for (const Polyline &polyline : polylines)
|
|
for (size_t i = 1; i + 1 < polyline.size(); ++i) {
|
|
const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized();
|
|
const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized();
|
|
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
|
}
|
|
return sharpest;
|
|
};
|
|
auto point_count = [](const Slic3r::Polylines &polylines) {
|
|
return std::accumulate(polylines.begin(), polylines.end(), size_t(0),
|
|
[](size_t count, const Polyline &polyline) { return count + polyline.size(); });
|
|
};
|
|
|
|
const Slic3r::Polylines sharp = fill(0.);
|
|
const Slic3r::Polylines smooth = fill(1.);
|
|
|
|
REQUIRE(!sharp.empty());
|
|
REQUIRE(smooth.size() == sharp.size());
|
|
REQUIRE(point_count(smooth) > point_count(sharp));
|
|
// The cell corners turn by 60 degrees; smoothing replaces them by gentle curves.
|
|
REQUIRE(sharpest_turn_cosine(sharp) < 0.6);
|
|
REQUIRE(sharpest_turn_cosine(smooth) > 0.9);
|
|
}
|
|
|
|
// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print.
|
|
// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones.
|
|
struct SparseInfillShape {
|
|
size_t point_count { 0 };
|
|
size_t sharp_turns { 0 };
|
|
size_t path_count { 0 };
|
|
double length { 0. };
|
|
// Digest of every point in the order it is printed. The counts above all survive the same
|
|
// extrusions being joined into different polylines, so only this tells two such fills apart.
|
|
uint64_t sequence { 14695981039346656037ull };
|
|
};
|
|
|
|
static SparseInfillShape sparse_infill_shape(const Print &print)
|
|
{
|
|
SparseInfillShape shape;
|
|
|
|
auto account = [&shape](const ExtrusionPath &path) {
|
|
if (!sparse_role(path.role()))
|
|
return;
|
|
const Points3 &pts = path.polyline.points;
|
|
++shape.path_count;
|
|
shape.point_count += pts.size();
|
|
for (const auto &pt : pts)
|
|
for (const coord_t coordinate : {pt.x(), pt.y(), pt.z()})
|
|
shape.sequence = (shape.sequence ^ uint64_t(coordinate)) * 1099511628211ull;
|
|
for (size_t i = 1; i < pts.size(); ++i)
|
|
shape.length += (pts[i] - pts[i - 1]).head<2>().cast<double>().norm();
|
|
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
|
const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast<double>();
|
|
const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast<double>();
|
|
if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. &&
|
|
incoming.normalized().dot(outgoing.normalized()) < 0.9)
|
|
++shape.sharp_turns;
|
|
}
|
|
};
|
|
|
|
for (const Layer *layer : print.objects().front()->layers())
|
|
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);
|
|
}
|
|
return shape;
|
|
}
|
|
|
|
TEST_CASE("Lightning infill slices the same model the same way twice", "[Fill][Regression]")
|
|
{
|
|
// Slicing twice in one process catches a generator that carries state from one slice to the
|
|
// next, or whose result depends on how the parallel layer fill interleaves.
|
|
auto shape = [] {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "lightning"},
|
|
{"sparse_infill_density", "50%"},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape first = shape();
|
|
const SparseInfillShape second = shape();
|
|
|
|
REQUIRE(first.path_count > 0);
|
|
REQUIRE(second.path_count == first.path_count);
|
|
REQUIRE(second.point_count == first.point_count);
|
|
REQUIRE(second.sharp_turns == first.sharp_turns);
|
|
// No tolerance: the same extrusions in the same order add up to the very same number.
|
|
REQUIRE_THAT(second.length, Catch::Matchers::WithinAbs(first.length, 0.));
|
|
// All of the above agree when the same branches are joined into different polylines, so the
|
|
// point sequence is what actually decides whether the two slices produced the same infill.
|
|
REQUIRE(second.sequence == first.sequence);
|
|
}
|
|
|
|
TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]")
|
|
{
|
|
auto shape_for = [](const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "lightning"},
|
|
{"sparse_infill_density", "15%"},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for("0%");
|
|
const SparseInfillShape smooth = shape_for("100%");
|
|
|
|
REQUIRE(sharp.point_count > 0);
|
|
// The branch turns are replaced by curves, which cut the corners off and take more points to
|
|
// describe. The turns where two branches are joined into one path stay sharp.
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
REQUIRE(smooth.length < sharp.length);
|
|
}
|
|
|
|
TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]")
|
|
{
|
|
auto shape_for = [](const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "concentric"},
|
|
{"sparse_infill_density", "20%"},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for("0%");
|
|
const SparseInfillShape smooth = shape_for("100%");
|
|
|
|
REQUIRE(sharp.point_count > 0);
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
REQUIRE(smooth.length < sharp.length);
|
|
}
|
|
|
|
TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]")
|
|
{
|
|
auto shape_for = [](const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "crosshatch"},
|
|
{"sparse_infill_density", "20%"},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for("0%");
|
|
const SparseInfillShape smooth = shape_for("100%");
|
|
|
|
REQUIRE(sharp.point_count > 0);
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
REQUIRE(smooth.length < sharp.length);
|
|
}
|
|
|
|
TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]")
|
|
{
|
|
auto shape_for = [](int multiline, const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "grid"},
|
|
{"sparse_infill_density", "20%"},
|
|
{"fill_multiline", multiline},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for(2, "0%");
|
|
const SparseInfillShape smooth = shape_for(2, "100%");
|
|
|
|
REQUIRE(sharp.point_count > 0);
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
REQUIRE(smooth.length < sharp.length);
|
|
|
|
// A single line per infill wall is the plain crossing line grid, which has no corner of its own.
|
|
const SparseInfillShape single_sharp = shape_for(1, "0%");
|
|
const SparseInfillShape single_smooth = shape_for(1, "100%");
|
|
REQUIRE(single_sharp.point_count > 0);
|
|
REQUIRE(single_smooth.point_count == single_sharp.point_count);
|
|
REQUIRE(single_smooth.length == single_sharp.length);
|
|
}
|
|
|
|
TEST_CASE("Multiline infill of an object matches the infill of a larger object with the same center", "[Fill]")
|
|
{
|
|
const InfillPattern pattern = GENERATE(ipGrid, ipTriangles, ipStars, ipCubic);
|
|
const int multiline = GENERATE(2, 3);
|
|
// A square with cells as large as itself, whose corners are as far out as the object bounding box
|
|
// reaches, and a strip with small cells, whose extents change with every layer orientation.
|
|
const auto [half, density] = GENERATE(table<Vec2d, float>({ { Vec2d(20., 20.), 0.15f }, { Vec2d(60., 4.), 0.35f } }));
|
|
CAPTURE(pattern, multiline, half.x(), half.y(), density);
|
|
|
|
// Off the origin; the same center gives both objects the same pattern.
|
|
auto rectangle = [](const Vec2d &half) {
|
|
const Vec2d center(100., 60.);
|
|
return ExPolygon{ Points{ Point::new_scale(center.x() - half.x(), center.y() - half.y()),
|
|
Point::new_scale(center.x() + half.x(), center.y() - half.y()),
|
|
Point::new_scale(center.x() + half.x(), center.y() + half.y()),
|
|
Point::new_scale(center.x() - half.x(), center.y() + half.y()) } };
|
|
};
|
|
const ExPolygon object = rectangle(half);
|
|
const ExPolygon larger = rectangle(half + Vec2d(10., 10.));
|
|
auto fill = [pattern, multiline, density = density](const ExPolygon ®ion, size_t layer_id) {
|
|
std::unique_ptr<Fill> filler(Fill::new_from_type(pattern));
|
|
filler->spacing = 0.45;
|
|
filler->angle = float(M_PI / 7.);
|
|
filler->fixed_angle = true;
|
|
filler->layer_id = layer_id;
|
|
filler->z = 0.2 * double(layer_id + 1);
|
|
filler->set_bounding_box(get_extents(region.contour));
|
|
|
|
FillParams params;
|
|
params.density = density;
|
|
params.multiline = multiline;
|
|
params.dont_adjust = true;
|
|
Surface surface(stInternal, region);
|
|
return filler->fill_surface(&surface, params);
|
|
};
|
|
// Away from the boundary of the object, where both are clipped and connected the same way.
|
|
const Polygons inner = shrink(to_polygons(object), scale_(1.));
|
|
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
|
|
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
|
|
double distance = 0.;
|
|
for (const Polyline &path : intersection_pl(from, inner))
|
|
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
|
|
distance = std::max(distance, tree.distance_from_lines<false>(point));
|
|
return unscale<double>(distance);
|
|
};
|
|
|
|
// Both layer orientations of Grid, all three of the triangular family.
|
|
for (size_t layer_id = 0; layer_id < 3; ++layer_id) {
|
|
CAPTURE(layer_id);
|
|
const Polylines walls = fill(object, layer_id);
|
|
REQUIRE_FALSE(walls.empty());
|
|
CHECK(get_intersections(to_lines(walls)).empty());
|
|
const Polylines reference = fill(larger, layer_id);
|
|
CHECK(farthest(reference, walls) < 0.01);
|
|
CHECK(farthest(walls, reference) < 0.01);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Gyroid infill of an object matches the infill of a larger object with the same center", "[Fill]")
|
|
{
|
|
const bool optimized = GENERATE(false, true);
|
|
const int multiline = GENERATE(1, 2);
|
|
const float density = GENERATE(0.05f, 0.2f);
|
|
const double spacing = 0.45;
|
|
CAPTURE(optimized, multiline, density);
|
|
|
|
auto circle = [](double radius) {
|
|
Polygon contour = make_circle_num_segments(scale_(radius), 120);
|
|
contour.translate(Point::new_scale(100., 60.));
|
|
return ExPolygon(std::move(contour));
|
|
};
|
|
const ExPolygon object = circle(20.);
|
|
const ExPolygon larger = circle(30.);
|
|
auto fill = [optimized, multiline, density, spacing](const ExPolygon ®ion, double z) {
|
|
std::unique_ptr<Fill> filler(Fill::new_from_type(ipGyroid));
|
|
filler->spacing = spacing;
|
|
filler->angle = float(M_PI / 7.);
|
|
filler->z = z;
|
|
|
|
FillParams params;
|
|
params.density = density;
|
|
params.multiline = multiline;
|
|
params.gyroid_optimized = optimized;
|
|
params.dont_adjust = true;
|
|
Surface surface(stInternal, region);
|
|
return filler->fill_surface(&surface, params);
|
|
};
|
|
// Away from the boundary of the object, where both are clipped and connected the same way.
|
|
const Polygons inner = shrink(to_polygons(object), scale_(1.));
|
|
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
|
|
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
|
|
double distance = 0.;
|
|
for (const Polyline &path : intersection_pl(from, inner))
|
|
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
|
|
distance = std::max(distance, tree.distance_from_lines<false>(point));
|
|
return unscale<double>(distance);
|
|
};
|
|
|
|
// Marching squares simplifies rings that start elsewhere in each object.
|
|
const double tolerance = optimized ? SPARSE_INFILL_RESOLUTION + 0.01 : 0.01;
|
|
// Half a z period of the waves, through both switches between horizontal and vertical waves.
|
|
const double wave_distance = spacing * multiline / (density * FillGyroid::DensityAdjust);
|
|
for (int step = 0; step <= 8; ++step) {
|
|
const double z = wave_distance * M_PI * step / 8.;
|
|
CAPTURE(z);
|
|
const Polylines paths = fill(object, z);
|
|
REQUIRE_FALSE(paths.empty());
|
|
const Polylines reference = fill(larger, z);
|
|
CHECK(farthest(reference, paths) < tolerance);
|
|
CHECK(farthest(paths, reference) < tolerance);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
|
|
{
|
|
const int multiline = GENERATE(2, 3);
|
|
const double spacing = 0.45;
|
|
const double density = 0.3;
|
|
const double wall = multiline * spacing;
|
|
CAPTURE(multiline);
|
|
|
|
const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.),
|
|
Point::new_scale(40., 40.), Point::new_scale(0., 40.) } };
|
|
auto fill = [®ion, spacing](int lines, double density, size_t layer_id, double z) {
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("cubic"));
|
|
filler->spacing = spacing;
|
|
filler->angle = float(M_PI / 7.);
|
|
filler->layer_id = layer_id;
|
|
filler->z = z;
|
|
|
|
FillParams params;
|
|
params.density = float(density);
|
|
params.multiline = lines;
|
|
params.dont_adjust = true;
|
|
params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary.
|
|
Slic3r::Surface surface(stInternal, region);
|
|
return filler->fill_surface(&surface, params);
|
|
};
|
|
// Away from the boundary, where a line is clipped earlier than the side of its wall.
|
|
const Polygons inner = shrink(to_polygons(region), scale_(3.));
|
|
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
|
|
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
|
|
double distance = 0.;
|
|
for (const Polyline &path : intersection_pl(from, inner))
|
|
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
|
|
distance = std::max(distance, tree.distance_from_lines<false>(point));
|
|
return unscale<double>(distance);
|
|
};
|
|
|
|
// One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing.
|
|
const double z_period = std::sqrt(2.) * wall / density;
|
|
const size_t layers = 30;
|
|
for (size_t layer_id = 0; layer_id < layers; ++layer_id) {
|
|
const double z = z_period * (layer_id + 0.5) / layers;
|
|
CAPTURE(layer_id, z);
|
|
const Polylines walls = fill(multiline, density, layer_id, z);
|
|
REQUIRE_FALSE(walls.empty());
|
|
CHECK(get_intersections(to_lines(walls)).empty());
|
|
// Long paths running out to the boundary, not loops around the cells.
|
|
CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); }));
|
|
|
|
// Single lines at the same spacing: the walls are drawn along them.
|
|
const Polylines lines = fill(1, density / multiline, layer_id, z);
|
|
REQUIRE_FALSE(lines.empty());
|
|
CHECK(farthest(lines, walls) < 0.5 * wall);
|
|
CHECK(farthest(walls, lines) < 1.5 * wall);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]")
|
|
{
|
|
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
|
|
const int multiline = GENERATE(2, 3);
|
|
CAPTURE(pattern, multiline);
|
|
|
|
// A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer.
|
|
TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm);
|
|
sphere.scale(0.3f);
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({sphere}, print,
|
|
{{"sparse_infill_pattern", pattern},
|
|
{"sparse_infill_density", "40%"},
|
|
{"fill_multiline", multiline},
|
|
{"infill_anchor", 0},
|
|
{"infill_anchor_max", 0},
|
|
{"layer_height", 0.3}});
|
|
|
|
size_t paths = 0, loops = 0;
|
|
for (const Layer *layer : print.objects().front()->layers()) {
|
|
Polylines printed;
|
|
Polygons sparse;
|
|
double spacing = 0.;
|
|
for (const LayerRegion *region : layer->regions()) {
|
|
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
|
if (entity->role() == erInternalInfill)
|
|
entity->collect_polylines(printed);
|
|
for (const Surface &surface : region->fill_surfaces.surfaces)
|
|
if (surface.surface_type == stInternal)
|
|
append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.)));
|
|
spacing = region->flow(frInfill).spacing();
|
|
}
|
|
if (printed.empty())
|
|
continue;
|
|
CAPTURE(layer->print_z);
|
|
paths += printed.size();
|
|
loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); });
|
|
CHECK(get_intersections(to_lines(printed)).empty());
|
|
|
|
// Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on.
|
|
// Pieces of one line that meet end to end are one line.
|
|
std::vector<size_t> line_of(printed.size());
|
|
std::iota(line_of.begin(), line_of.end(), 0);
|
|
std::function<size_t(size_t)> find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); };
|
|
for (size_t i = 0; i < printed.size(); ++i)
|
|
for (size_t j = i + 1; j < printed.size(); ++j)
|
|
for (const Point &a : { printed[i].first_point(), printed[i].last_point() })
|
|
for (const Point &b : { printed[j].first_point(), printed[j].last_point() })
|
|
if ((a - b).cast<double>().norm() < SCALED_EPSILON)
|
|
line_of[find(i)] = find(j);
|
|
Lines lines;
|
|
std::vector<size_t> owner;
|
|
for (size_t i = 0; i < printed.size(); ++i)
|
|
for (const Line &line : printed[i].lines()) {
|
|
lines.push_back(line);
|
|
owner.push_back(find(i));
|
|
}
|
|
AABBTreeLines::LinesDistancer<Line> tree(lines);
|
|
double closest = spacing;
|
|
for (size_t i = 0; i < printed.size(); ++i)
|
|
for (const Point &p : printed[i].equally_spaced_points(scale_(0.1)))
|
|
if (contains(sparse, p))
|
|
for (size_t k : tree.all_lines_in_radius(p, scale_(spacing)))
|
|
if (owner[k] != find(i))
|
|
closest = std::min(closest, unscale<double>(lines[k].distance_to(p)));
|
|
CHECK(closest > 0.45 * spacing);
|
|
}
|
|
REQUIRE(paths > 0);
|
|
// The lines run on through the cells instead of each cell getting its own loops.
|
|
CHECK(loops < paths / 4);
|
|
}
|
|
|
|
TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]")
|
|
{
|
|
const int sweep = GENERATE(0, 1, 2);
|
|
// Offset of the third family in walls, so the three meet in points or in small triangles.
|
|
const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.);
|
|
// Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0.
|
|
const bool starting = GENERATE(false, true);
|
|
CAPTURE(sweep, shift, starting);
|
|
|
|
const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.);
|
|
Lines lines;
|
|
for (int k = 0; k < 3; ++k) {
|
|
const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x());
|
|
for (int i = -6; i <= 6; ++i) {
|
|
const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal;
|
|
const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch;
|
|
lines.emplace_back((mid + start * dir).cast<coord_t>(), (mid + 10. * pitch * dir).cast<coord_t>());
|
|
}
|
|
}
|
|
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.)));
|
|
REQUIRE_FALSE(paths.empty());
|
|
CHECK(get_intersections(to_lines(paths)).empty());
|
|
|
|
Lines pieces;
|
|
std::vector<size_t> owner;
|
|
for (size_t i = 0; i < paths.size(); ++i)
|
|
for (const Line &line : paths[i].lines()) {
|
|
pieces.push_back(line);
|
|
owner.push_back(i);
|
|
}
|
|
AABBTreeLines::LinesDistancer<Line> tree(pieces);
|
|
auto clearance = [&](size_t i) {
|
|
const Line &a = pieces[i];
|
|
double distance = std::numeric_limits<double>::max();
|
|
for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1))
|
|
if (owner[j] != owner[i]) {
|
|
const Line &b = pieces[j];
|
|
distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) });
|
|
}
|
|
return distance;
|
|
};
|
|
auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; };
|
|
|
|
double closest = std::numeric_limits<double>::max();
|
|
for (size_t i = 0; i < pieces.size(); ++i)
|
|
if (inside(pieces[i].midpoint()))
|
|
closest = std::min(closest, clearance(i));
|
|
CHECK(closest > 0.99 * d1);
|
|
|
|
// Each path at a crossing touches another one there, none stops short of it.
|
|
double widest = 0.;
|
|
for (size_t i = 0; i < lines.size(); ++i)
|
|
for (size_t j = i + 1; j < lines.size(); ++j)
|
|
if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) {
|
|
std::map<size_t, double> at;
|
|
for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1))
|
|
at.emplace(owner[k], std::numeric_limits<double>::max());
|
|
for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1))
|
|
if (auto it = at.find(owner[k]); it != at.end())
|
|
it->second = std::min(it->second, clearance(k));
|
|
for (const auto &path : at)
|
|
widest = std::max(widest, path.second);
|
|
}
|
|
CHECK(widest < 1.02 * d1);
|
|
}
|
|
|
|
TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]")
|
|
{
|
|
const int sweep = GENERATE(0, 1, 2);
|
|
// Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line.
|
|
const double start = GENERATE(0.3, 0.6, 1., 2.);
|
|
CAPTURE(sweep, start);
|
|
|
|
const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.);
|
|
const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.));
|
|
const Vec2d on_horizontal = start * d1 * horizontal;
|
|
const Lines lines{ Line((-length * diagonal).cast<coord_t>(), (length * diagonal).cast<coord_t>()),
|
|
Line(Point(0, 0), (length * horizontal).cast<coord_t>()),
|
|
Line(on_horizontal.cast<coord_t>(), (on_horizontal - length * steep).cast<coord_t>()) };
|
|
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.)));
|
|
|
|
// The end of the path along each line nearest to where that line starts.
|
|
auto end_along = [&paths](const Line &line) {
|
|
for (const Polyline &path : paths)
|
|
if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON)
|
|
return (path.first_point() - line.a).cast<double>().norm() < (path.last_point() - line.a).cast<double>().norm() ? path.first_point() : path.last_point();
|
|
return Point(std::numeric_limits<coord_t>::max(), 0);
|
|
};
|
|
const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]);
|
|
REQUIRE(horizontal_end.x() != std::numeric_limits<coord_t>::max());
|
|
REQUIRE(steep_end.x() != std::numeric_limits<coord_t>::max());
|
|
// Both reach the overlap into the wall of the path they stop at, none stops short of it.
|
|
CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01));
|
|
CHECK(lines[1].distance_to(steep_end) / d1 < 0.91);
|
|
CHECK(get_intersections(to_lines(paths)).empty());
|
|
}
|
|
|
|
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
|
|
{
|
|
auto shape_for = [](const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "3dhoneycomb"},
|
|
{"sparse_infill_density", "20%"},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for("0%");
|
|
const SparseInfillShape smooth = shape_for("100%");
|
|
|
|
REQUIRE(sharp.point_count > 0);
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
REQUIRE(smooth.length < sharp.length);
|
|
}
|
|
|
|
TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]")
|
|
{
|
|
// The concentric loops are offsets of the fill region and are never clipped to it, so a corner
|
|
// rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of
|
|
// the turn, which leaves the region at every corner of a hole, and in a region thinner than the
|
|
// curve even at a corner turning inwards.
|
|
const bool thin_region = GENERATE(false, true);
|
|
ExPolygon region;
|
|
if (thin_region) {
|
|
// An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them.
|
|
region = ExPolygon{ Slic3r::Points{
|
|
Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2),
|
|
Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } };
|
|
} else {
|
|
region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.),
|
|
Point::new_scale(50., 50.), Point::new_scale(0., 50.) },
|
|
Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.),
|
|
Point::new_scale(20., 30.), Point::new_scale(20., 20.) } };
|
|
}
|
|
CAPTURE(thin_region);
|
|
|
|
auto fill = [®ion](double smooth_factor) {
|
|
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("concentric"));
|
|
filler->spacing = 0.45;
|
|
|
|
FillParams params;
|
|
params.density = 0.1f;
|
|
params.dont_adjust = true;
|
|
params.smooth_factor = smooth_factor;
|
|
|
|
Slic3r::Surface surface(stInternal, region);
|
|
return filler->fill_surface(&surface, params);
|
|
};
|
|
auto point_count = [](const Slic3r::Polylines &polylines) {
|
|
return std::accumulate(polylines.begin(), polylines.end(), size_t(0),
|
|
[](size_t count, const Polyline &polyline) { return count + polyline.size(); });
|
|
};
|
|
|
|
const Slic3r::Polylines sharp = fill(0.);
|
|
const Slic3r::Polylines smooth = fill(1.);
|
|
REQUIRE(!sharp.empty());
|
|
|
|
// Nothing leaves the fill region, which the unrounded loops already touch from the inside.
|
|
const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON));
|
|
REQUIRE(diff_pl(sharp, bounds).empty());
|
|
REQUIRE(diff_pl(smooth, bounds).empty());
|
|
// The corners that the region has room for are still rounded.
|
|
if (!thin_region)
|
|
REQUIRE(point_count(smooth) > point_count(sharp));
|
|
}
|
|
|
|
TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]")
|
|
{
|
|
// With more than one line per infill wall, the branches are printed as outlines drawn around them,
|
|
// and the outlines of branches that run close to each other merge into one. Rounding the branches
|
|
// before those outlines are built moves them apart, which breaks the merged outlines up into
|
|
// separate loops - many more of them, each needing its own travel move.
|
|
auto shape_for = [](const std::string &smooth_factor) {
|
|
Print print;
|
|
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
|
{{"sparse_infill_pattern", "lightning"},
|
|
{"sparse_infill_density", "50%"},
|
|
{"fill_multiline", 2},
|
|
{"sparse_infill_smooth_factor", smooth_factor},
|
|
{"layer_height", 0.2}});
|
|
return sparse_infill_shape(print);
|
|
};
|
|
|
|
const SparseInfillShape sharp = shape_for("0%");
|
|
const SparseInfillShape smooth = shape_for("100%");
|
|
|
|
REQUIRE(sharp.path_count > 0);
|
|
// The loop count varies by a loop or two between platforms and between runs, so this is not an
|
|
// exact comparison. Smoothing should leave it about where it was; uncapping the smoothing
|
|
// reach, the regression this guards against, adds about 10%.
|
|
const size_t allowed_extra = sharp.path_count / 50; // 2%
|
|
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
|
|
// The outlines are still rounded.
|
|
REQUIRE(smooth.point_count > sharp.point_count);
|
|
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
|
}
|
|
|
|
TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][InternalBridge][Regression]")
|
|
{
|
|
// Orca: Compare generated anchors with actual extrusion across plane-path patterns,
|
|
// smoothing, multiline and rotations; an origin shift must not pass as valid support.
|
|
const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords");
|
|
const std::string smoothing = GENERATE("0%", "100%");
|
|
const int multiline = GENERATE(1, 2);
|
|
const bool rotated = GENERATE(false, true);
|
|
const bool separated = GENERATE(false, true);
|
|
CAPTURE(pattern, smoothing, multiline, rotated, separated);
|
|
|
|
auto config = DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
|
|
{"sparse_infill_density", "15%"},
|
|
{"sparse_infill_smooth_factor", smoothing},
|
|
{"fill_multiline", multiline},
|
|
{"infill_direction", 45},
|
|
{"sparse_infill_rotate_template", rotated ? "0,25,50" : ""},
|
|
{"align_infill_direction_to_model", rotated},
|
|
{"separated_infills", separated},
|
|
{"top_shell_layers", 0},
|
|
{"bottom_shell_layers", 0},
|
|
{"top_shell_thickness", 0},
|
|
{"bottom_shell_thickness", 0},
|
|
{"layer_height", 0.2},
|
|
{"initial_layer_print_height", 0.2},
|
|
{"resolution", 0.012}});
|
|
Print print;
|
|
Model model;
|
|
TriangleMesh mesh = make_cube(30, 24, 1);
|
|
if (separated) {
|
|
// Orca: Two disconnected bodies in one object must each use their own infill origin.
|
|
TriangleMesh second = make_cube(30, 24, 1);
|
|
second.translate(50, 0, 0);
|
|
mesh.merge(second);
|
|
}
|
|
Slic3r::Test::init_print({mesh}, print, model, config, nullptr, false);
|
|
if (rotated) {
|
|
model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(23.)));
|
|
print.apply(model, config);
|
|
}
|
|
print.process();
|
|
|
|
const Layer &layer = *print.objects().front()->get_layer(4);
|
|
Polylines printed;
|
|
for (const LayerRegion *region : layer.regions())
|
|
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
|
if (entity->role() == erInternalInfill)
|
|
entity->collect_polylines(printed);
|
|
REQUIRE_FALSE(printed.empty());
|
|
const AABBTreeLines::LinesDistancer<Line> printed_tree(to_lines(printed));
|
|
|
|
// Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently.
|
|
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr),
|
|
shrink(to_polygons(layer.lslices), scale_(3.)));
|
|
REQUIRE_FALSE(anchors.empty());
|
|
double max_distance = 0.;
|
|
for (const Polyline &path : anchors)
|
|
for (const Point &point : path.equally_spaced_points(scale_(0.25)))
|
|
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
|
|
// Orca: Allow only the configured simplification tolerance; infill-scale offsets
|
|
// would hide anchors that no longer coincide with printed lines.
|
|
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
|
|
}
|