Further Perl unit test porting to C++ and Perl interface reduction:

Ported cooling, gap fill, thin walls and polyline unit tests.
This commit is contained in:
Vojtech Bubnik 2022-05-05 17:57:48 +02:00
parent 5a67d0e183
commit d4b8d4d0f3
46 changed files with 1080 additions and 1113 deletions

View file

@ -3,11 +3,14 @@ add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_avoid_crossing_perimeters.cpp
test_bridges.cpp
test_cooling.cpp
test_custom_gcode.cpp
test_data.cpp
test_data.hpp
test_extrusion_entity.cpp
test_fill.cpp
test_flow.cpp
test_gaps.cpp
test_gcode.cpp
test_gcodefindreplace.cpp
test_gcodewriter.cpp
@ -19,6 +22,7 @@ add_executable(${_TEST_NAME}_tests
test_printobject.cpp
test_skirt_brim.cpp
test_support_material.cpp
test_thin_walls.cpp
test_trianglemesh.cpp
)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)

View file

@ -0,0 +1,274 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/GCode/CoolingBuffer.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
std::unique_ptr<CoolingBuffer> make_cooling_buffer(
GCode &gcode,
const DynamicPrintConfig &config = DynamicPrintConfig{},
const std::vector<unsigned int> &extruder_ids = { 0 })
{
PrintConfig print_config;
print_config.apply(config, true); // ignore_nonexistent
gcode.apply_print_config(print_config);
gcode.set_layer_count(10);
gcode.writer().set_extruders(extruder_ids);
gcode.writer().set_extruder(0);
return std::make_unique<CoolingBuffer>(gcode);
}
SCENARIO("Cooling unit tests", "[Cooling]") {
const std::string gcode1 = "G1 X100 E1 F3000\n";
// 2 sec
const double print_time1 = 100. / (3000. / 60.);
const std::string gcode2 = gcode1 + "G1 X0 E1 F3000\n";
// 4 sec
const double print_time2 = 2. * print_time1;
auto config = DynamicPrintConfig::full_print_config_with({
// Default cooling settings.
{ "bridge_fan_speed", "100" },
{ "cooling", "1" },
{ "fan_always_on", "0" },
{ "fan_below_layer_time", "60" },
{ "max_fan_speed", "100" },
{ "min_print_speed", "10" },
{ "slowdown_below_layer_time", "5" },
// Default print speeds.
{ "bridge_speed", 60 },
{ "external_perimeter_speed", "50%" },
{ "first_layer_speed", 30 },
{ "gap_fill_speed", 20 },
{ "infill_speed", 80 },
{ "perimeter_speed", 60 },
{ "small_perimeter_speed", 15 },
{ "solid_infill_speed", 20 },
{ "top_solid_infill_speed", 15 },
{ "max_print_speed", 80 },
// Override for tests.
{ "disable_fan_first_layers", "0" }
});
WHEN("G-code block 3") {
THEN("speed is not altered when elapsed time is greater than slowdown threshold") {
// Print time of gcode.
const double print_time = 100. / (3000. / 60.);
//FIXME slowdown_below_layer_time is rounded down significantly from 1.8s to 1s.
config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 0.999) } } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer("G1 F3000;_EXTRUDE_SET_SPEED\nG1 X100 E1", 0, true);
bool speed_not_altered = gcode.find("F3000") != gcode.npos;
REQUIRE(speed_not_altered);
}
}
WHEN("G-code block 4") {
const std::string gcode_src =
"G1 X50 F2500\n"
"G1 F3000;_EXTRUDE_SET_SPEED\n"
"G1 X100 E1\n"
";_EXTRUDE_END\n"
"G1 E4 F400";
// Print time of gcode.
const double print_time = 50. / (2500. / 60.) + 100. / (3000. / 60.) + 4. / (400. / 60.);
config.set_deserialize_strict({ { "slowdown_below_layer_time", { int(print_time * 1.001) } } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode_src, 0, true);
THEN("speed is altered when elapsed time is lower than slowdown threshold") {
bool speed_is_altered = gcode.find("F3000") == gcode.npos;
REQUIRE(speed_is_altered);
}
THEN("speed is not altered for travel moves") {
bool speed_not_altered = gcode.find("F2500") != gcode.npos;
REQUIRE(speed_not_altered);
}
THEN("speed is not altered for extruder-only moves") {
bool speed_not_altered = gcode.find("F400") != gcode.npos;
REQUIRE(speed_not_altered);
}
}
WHEN("G-code block 1") {
THEN("fan is not activated when elapsed time is greater than fan threshold") {
config.set_deserialize_strict({
{ "fan_below_layer_time" , int(print_time1 * 0.88) },
{ "slowdown_below_layer_time" , int(print_time1 * 0.99) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode1, 0, true);
bool fan_not_activated = gcode.find("M106") == gcode.npos;
REQUIRE(fan_not_activated);
}
}
WHEN("G-code block 1 with two extruders") {
config.set_deserialize_strict({
{ "cooling", "1, 0" },
{ "fan_below_layer_time", { int(print_time2 + 1.), int(print_time2 + 1.) } },
{ "slowdown_below_layer_time", { int(print_time2 + 2.), int(print_time2 + 2.) } }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config, { 0, 1 });
std::string gcode = buffer->process_layer(gcode1 + "T1\nG1 X0 E1 F3000\n", 0, true);
THEN("fan is activated for the 1st tool") {
bool ok = gcode.find("M106") == 0;
REQUIRE(ok);
}
THEN("fan is disabled for the 2nd tool") {
bool ok = gcode.find("\nM107") > 0;
REQUIRE(ok);
}
}
WHEN("G-code block 2") {
THEN("slowdown is computed on all objects printing at the same Z") {
config.set_deserialize_strict({ { "slowdown_below_layer_time", int(print_time2 * 0.99) } });
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
std::string gcode = buffer->process_layer(gcode2, 0, true);
bool ok = gcode.find("F3000") != gcode.npos;
REQUIRE(ok);
}
THEN("fan is not activated on all objects printing at different Z") {
config.set_deserialize_strict({
{ "fan_below_layer_time", int(print_time2 * 0.65) },
{ "slowdown_below_layer_time", int(print_time2 * 0.7) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
// use an elapsed time which is < the threshold but greater than it when summed twice
std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
bool fan_not_activated = gcode.find("M106") == gcode.npos;
REQUIRE(fan_not_activated);
}
THEN("fan is activated on all objects printing at different Z") {
// use an elapsed time which is < the threshold even when summed twice
config.set_deserialize_strict({
{ "fan_below_layer_time", int(print_time2 + 1) },
{ "slowdown_below_layer_time", int(print_time2 + 1) }
});
GCode gcodegen;
auto buffer = make_cooling_buffer(gcodegen, config);
// use an elapsed time which is < the threshold but greater than it when summed twice
std::string gcode = buffer->process_layer(gcode2, 0, true) + buffer->process_layer(gcode2, 1, true);
bool fan_activated = gcode.find("M106") != gcode.npos;
REQUIRE(fan_activated);
}
}
}
SCENARIO("Cooling integration tests", "[Cooling]") {
GIVEN("overhang") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "cooling", { 1 } },
{ "bridge_fan_speed", { 100 } },
{ "fan_below_layer_time", { 0 } },
{ "slowdown_below_layer_time", { 0 } },
{ "bridge_speed", 99 },
// internal bridges use solid_infil speed
{ "bottom_solid_layers", 1 },
// internal bridges use solid_infil speed
});
GCodeReader parser;
int fan = 0;
int fan_with_incorrect_speeds = 0;
int fan_with_incorrect_print_speeds = 0;
int bridge_with_no_fan = 0;
const double bridge_speed = config.opt_float("bridge_speed") * 60;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config),
[&fan, &fan_with_incorrect_speeds, &fan_with_incorrect_print_speeds, &bridge_with_no_fan, bridge_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("M106")) {
line.has_value('S', fan);
if (fan != 255)
++ fan_with_incorrect_speeds;
} else if (line.cmd_is("M107")) {
fan = 0;
} else if (line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), bridge_speed)) {
if (fan != 255)
++ bridge_with_no_fan;
} else {
if (fan != 0)
++ fan_with_incorrect_print_speeds;
}
}
});
THEN("bridge fan speed is applied correctly") {
REQUIRE(fan_with_incorrect_speeds == 0);
}
THEN("bridge fan is only turned on for bridges") {
REQUIRE(fan_with_incorrect_print_speeds == 0);
}
THEN("bridge fan is turned on for all bridges") {
REQUIRE(bridge_with_no_fan == 0);
}
}
GIVEN("20mm cube") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "cooling", { 1 } },
{ "fan_below_layer_time", { 0 } },
{ "slowdown_below_layer_time", { 10 } },
{ "min_print_speed", { 0 } },
{ "start_gcode", "" },
{ "first_layer_speed", "100%" },
{ "external_perimeter_speed", 99 }
});
GCodeReader parser;
const double external_perimeter_speed = config.opt<ConfigOptionFloatOrPercent>("external_perimeter_speed")->value * 60;
std::vector<double> layer_times;
// z => 1
std::map<coord_t, int> layer_external;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&layer_times, &layer_external, external_perimeter_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("G1")) {
if (line.dist_Z(self) != 0) {
layer_times.emplace_back(0.);
layer_external[scaled<coord_t>(line.new_Z(self))] = 0;
}
double l = line.dist_XY(self);
if (l == 0)
l = line.dist_E(self);
if (l == 0)
l = line.dist_Z(self);
if (l > 0.) {
if (layer_times.empty())
layer_times.emplace_back(0.);
layer_times.back() += 60. * std::abs(l) / line.new_F(self);
}
if (line.has('F') && line.f() == external_perimeter_speed)
++ layer_external[scaled<coord_t>(self.z())];
}
});
THEN("slowdown_below_layer_time is honored") {
// Account for some inaccuracies.
const double slowdown_below_layer_time = config.opt<ConfigOptionInts>("slowdown_below_layer_time")->values.front() - 0.2;
size_t minimum_time_honored = std::count_if(layer_times.begin(), layer_times.end(),
[slowdown_below_layer_time](double t){ return t > slowdown_below_layer_time; });
REQUIRE(minimum_time_honored == layer_times.size());
}
THEN("slowdown_below_layer_time does not alter external perimeters") {
// Broken by Vojtech
// check that all layers have at least one unaltered external perimeter speed
// my $external = all { $_ > 0 } values %layer_external;
// ok $external, '';
}
}
}

View file

@ -0,0 +1,220 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/Config.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
#if 0
SCENARIO("Output file format", "[CustomGCode]")
{
WHEN("output_file_format set") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "output_filename_format", "ts_[travel_speed]_lh_[layer_height].gcode" },
{ "start_gcode", "TRAVEL:[travel_speed] HEIGHT:[layer_height]\n" }
});
Print print;
Model model;
Test::init_print({ Test::TestMesh::cube_2x20x10 }, print, model, config);
std::string output_file = print.output_filepath();
THEN("print config options are replaced in output filename") {
output_file.find(std::string("ts_") + )
}
my ($t, $h) = map $config->$_, qw(travel_speed layer_height);
ok $output_file =~ /ts_${t}_/, '';
ok $output_file =~ /lh_$h\./, 'region config options are replaced in output filename';
std::string gcode = print.gcode(print);
THEN("print config options are replaced in custom G-code") {
ok $gcode =~ /TRAVEL:$t/, '';
}
THEN("region config options are replaced in custom G-code") {
ok $gcode =~ /HEIGHT:$h/, '';
}
}
}
SCENARIO("Custom G-code", "[CustomGCode]")
{
WHEN("start_gcode and layer_gcode set") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "start_gcode", "_MY_CUSTOM_START_GCODE_" }, // to avoid dealing with the nozzle lift in start G-code
{ "layer_gcode", "_MY_CUSTOM_LAYER_GCODE_" }
});
GCodeReader parser;
bool last_move_was_z_change = false;
int num_layer_changes_not_applied = 0;
parser.parse_buffer(Slic3r::Test::slice({ Test::TestMesh::cube_2x20x10 }, config),
[&last_move_was_z_change, &num_layer_changes_not_applied](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self)) {
if (! was_extruding)
seam_points.emplace_back(self.xy_scaled());
was_extruding = true;
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
was_extruding = false;
}
if (last_move_was_z_change != line.cmd_is("_MY_CUSTOM_LAYER_GCODE_"))
++ num_layer_changes_not_applied;
last_move_was_z_change = line.dist_Z(self) > 0;
});
THEN("custom layer G-code is applied after Z move and before other moves");
};
{
my $config = Slic3r::Config->new;
$config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
$config->set('extruder', 2);
$config->set('first_layer_temperature', [200,205]);
{
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
ok $gcode =~ /M104 S205 T1/, 'temperature set correctly for non-zero yet single extruder';
ok $gcode !~ /M104 S\d+ T0/, 'unused extruder correctly ignored';
}
$config->set('infill_extruder', 1);
{
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
ok $gcode =~ /M104 S200 T0/, 'temperature set correctly for first extruder';
ok $gcode =~ /M104 S205 T1/, 'temperature set correctly for second extruder';
}
my @start_gcode = (qq!
;__temp0:[first_layer_temperature_0]__
;__temp1:[first_layer_temperature_1]__
;__temp2:[first_layer_temperature_2]__
!, qq!
;__temp0:{first_layer_temperature[0]}__
;__temp1:{first_layer_temperature[1]}__
;__temp2:{first_layer_temperature[2]}__
!);
my @syntax_description = (' (legacy syntax)', ' (new syntax)');
for my $i (0, 1) {
$config->set('start_gcode', $start_gcode[$i]);
{
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
# we use the [infill_extruder] placeholder to make sure this test doesn't
# catch a false positive caused by the unparsed start G-code option itself
# being embedded in the G-code
ok $gcode =~ /temp0:200/, 'temperature placeholder for first extruder correctly populated' . $syntax_description[$i];
ok $gcode =~ /temp1:205/, 'temperature placeholder for second extruder correctly populated' . $syntax_description[$i];
ok $gcode =~ /temp2:200/, 'temperature placeholder for unused extruder populated with first value' . $syntax_description[$i];
}
}
$config->set('start_gcode', qq!
;substitution:{if infill_extruder==1}extruder1
{elsif infill_extruder==2}extruder2
{else}extruder3{endif}
!);
{
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
ok $gcode =~ /substitution:extruder1/, 'if / else / endif - first block returned';
}
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('before_layer_gcode', ';BEFORE [layer_num]');
$config->set('layer_gcode', ';CHANGE [layer_num]');
$config->set('support_material', 1);
$config->set('layer_height', 0.2);
my $print = Slic3r::Test::init_print('overhang', config => $config);
my $gcode = Slic3r::Test::gcode($print);
my @before = ();
my @change = ();
foreach my $line (split /\R+/, $gcode) {
if ($line =~ /;BEFORE (\d+)/) {
push @before, $1;
} elsif ($line =~ /;CHANGE (\d+)/) {
push @change, $1;
fail 'inconsistent layer_num before and after layer change'
if $1 != $before[-1];
}
}
is_deeply \@before, \@change, 'layer_num is consistent before and after layer changes';
ok !defined(first { $change[$_] != $change[$_-1]+1 } 1..$#change),
'layer_num grows continously'; # i.e. no duplicates or regressions
}
{
my $config = Slic3r::Config->new;
$config->set('nozzle_diameter', [0.6,0.6,0.6,0.6,0.6]);
$config->set('start_gcode', qq!
;substitution:{if infill_extruder==1}if block
{elsif infill_extruder==2}elsif block 1
{elsif infill_extruder==3}elsif block 2
{elsif infill_extruder==4}elsif block 3
{else}endif block{endif}
!);
my @returned = ('', 'if block', 'elsif block 1', 'elsif block 2', 'elsif block 3', 'endif block');
for my $i (1,2,3,4,5) {
$config->set('infill_extruder', $i);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
my $found_other = 0;
for my $j (1,2,3,4,5) {
next if $i == $j;
$found_other = 1 if $gcode =~ /substitution:$returned[$j]/;
}
ok $gcode =~ /substitution:$returned[$i]/, 'if / else / endif - ' . $returned[$i] . ' returned';
ok !$found_other, 'if / else / endif - only ' . $returned[$i] . ' returned';
}
}
{
my $config = Slic3r::Config->new;
$config->set('nozzle_diameter', [0.6,0.6,0.6,0.6]);
$config->set('start_gcode',
';substitution:{if infill_extruder==1}{if perimeter_extruder==1}block11{else}block12{endif}' .
'{elsif infill_extruder==2}{if perimeter_extruder==1}block21{else}block22{endif}' .
'{else}{if perimeter_extruder==1}block31{else}block32{endif}{endif}:end');
for my $i (1,2,3) {
$config->set('infill_extruder', $i);
for my $j (1,2) {
$config->set('perimeter_extruder', $j);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
ok $gcode =~ /substitution:block$i$j:end/, "two level if / else / endif - block$i$j returned";
}
}
}
{
my $config = Slic3r::Config->new;
$config->set('start_gcode',
';substitution:{if notes=="MK2"}MK2{elsif notes=="MK3"}MK3{else}MK1{endif}:end');
for my $printer_name ("MK2", "MK3", "MK1") {
$config->set('notes', $printer_name);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $gcode = Slic3r::Test::gcode($print);
ok $gcode =~ /substitution:$printer_name:end/, "printer name $printer_name matched";
}
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('complete_objects', 1);
$config->set('between_objects_gcode', '_MY_CUSTOM_GCODE_');
my $print = Slic3r::Test::init_print('20mm_cube', config => $config, duplicate => 3);
my $gcode = Slic3r::Test::gcode($print);
is scalar(() = $gcode =~ /^_MY_CUSTOM_GCODE_/gm), 2, 'between_objects_gcode is applied correctly';
}
#endif

View file

@ -26,6 +26,7 @@ const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names {
std::pair<TestMesh, const char*>(TestMesh::V, "V"),
std::pair<TestMesh, const char*>(TestMesh::_40x10, "40x10"),
std::pair<TestMesh, const char*>(TestMesh::cube_20x20x20, "cube_20x20x20"),
std::pair<TestMesh, const char*>(TestMesh::cube_2x20x10, "cube_2x20x10"),
std::pair<TestMesh, const char*>(TestMesh::sphere_50mm, "sphere_50mm"),
std::pair<TestMesh, const char*>(TestMesh::bridge, "bridge"),
std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole, "bridge_with_hole"),
@ -49,6 +50,9 @@ TriangleMesh mesh(TestMesh m)
case TestMesh::cube_20x20x20:
mesh = Slic3r::make_cube(20, 20, 20);
break;
case TestMesh::cube_2x20x10:
mesh = Slic3r::make_cube(2, 20, 10);
break;
case TestMesh::sphere_50mm:
mesh = Slic3r::make_sphere(50, PI / 243.0);
break;

View file

@ -21,6 +21,7 @@ enum class TestMesh {
V,
_40x10,
cube_20x20x20,
cube_2x20x10,
sphere_50mm,
bridge,
bridge_with_hole,

View file

@ -197,7 +197,7 @@ TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
SCENARIO("Infill does not exceed perimeters", "[Fill]")
{
auto test = [](const std::string_view pattern) {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.4, 0.4, 0.4, 0.4" },
{ "fill_pattern", pattern },
{ "top_fill_pattern", pattern },

View file

@ -5,8 +5,6 @@
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Flow.hpp"
@ -16,61 +14,118 @@ using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Extrusion width specifics", "[Flow]") {
GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") {
// this is a sharedptr
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "brim_width", 2 },
{ "skirts", 1 },
{ "perimeters", 3 },
{ "fill_density", "40%" },
{ "first_layer_height", 0.3 }
});
WHEN("first layer width set to 2mm") {
Slic3r::Model model;
config.set("first_layer_extrusion_width", 2);
Slic3r::Print print;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
std::vector<double> E_per_mm_bottom;
std::string gcode = Test::gcode(print);
Slic3r::GCodeReader parser;
const double layer_height = config.opt_float("layer_height");
parser.parse_buffer(gcode, [&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
if (line.extruding(self) && line.dist_XY(self) > 0) {
E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
}
});
THEN(" First layer width applies to everything on first layer.") {
bool pass = false;
double avg_E = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size());
pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [avg_E] (const double& v) { return v == Approx(avg_E); }) == 0);
REQUIRE(pass == true);
REQUIRE(E_per_mm_bottom.size() > 0); // make sure it actually passed because of extrusion
}
THEN(" First layer width does not apply to upper layer.") {
auto test = [](const DynamicPrintConfig &config) {
Slic3r::GCodeReader parser;
const double layer_height = config.opt_float("layer_height");
std::vector<double> E_per_mm_bottom;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line)
{
if (self.z() == Approx(layer_height).margin(0.01)) { // only consider first layer
if (line.extruding(self) && line.dist_XY(self) > 0)
E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
});
THEN("First layer width applies to everything on first layer.") {
REQUIRE(E_per_mm_bottom.size() > 0);
const double E_per_mm_avg = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast<double>(E_per_mm_bottom.size());
bool pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [E_per_mm_avg] (const double& v) { return v == Approx(E_per_mm_avg); }) == 0);
REQUIRE(pass);
}
THEN("First layer width does not apply to upper layer.") {
}
};
GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 1 },
{ "brim_width", 2 },
{ "perimeters", 3 },
{ "fill_density", "40%" },
{ "first_layer_height", 0.3 },
{ "first_layer_extrusion_width", "2" },
});
WHEN("Slicing a 20mm cube") {
test(config);
}
}
GIVEN("A config with more options and a 20mm cube ") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 1 },
{ "brim_width", 2 },
{ "perimeters", 3 },
{ "fill_density", "40%" },
{ "layer_height", "0.35" },
{ "first_layer_height", "0.35" },
{ "bottom_solid_layers", 1 },
{ "first_layer_extrusion_width", "2" },
{ "filament_diameter", "3" },
{ "nozzle_diameter", "0.5" }
});
WHEN("Slicing a 20mm cube") {
test(config);
}
}
}
// needs gcode export
SCENARIO(" Bridge flow specifics.", "[Flow]") {
auto config = DynamicPrintConfig::full_print_config_with({
{ "bridge_speed", 99 },
{ "bridge_flow_ratio", 1 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test = [](const DynamicPrintConfig &config) {
GCodeReader parser;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
std::vector<double> E_per_mm;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config),
[&E_per_mm, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), bridge_speed))
E_per_mm.emplace_back(line.dist_E(self) / line.dist_XY(self));
}
});
const double nozzle_dmr = config.opt<ConfigOptionFloats>("nozzle_diameter")->get_at(0);
const double filament_dmr = config.opt<ConfigOptionFloats>("filament_diameter")->get_at(0);
const double bridge_mm_per_mm = sqr(nozzle_dmr / filament_dmr) * config.opt_float("bridge_flow_ratio");
size_t num_errors = std::count_if(E_per_mm.begin(), E_per_mm.end(),
[bridge_mm_per_mm](double v){ return std::abs(v - bridge_mm_per_mm) > 0.01; });
return num_errors == 0;
};
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 1.0") {
WHEN("bridge_flow_ratio is set to 0.5 and extrusion width to default") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", "0" } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 0.5") {
WHEN("bridge_flow_ratio is set to 2.0 and extrusion width to default") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 2.0}, { "extrusion_width", "0" } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 2.0") {
WHEN("bridge_flow_ratio is set to 0.5 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 0.5}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 1.0 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 1.0}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
WHEN("bridge_flow_ratio is set to 2 and extrusion_width to 0.4") {
config.set_deserialize_strict({ { "bridge_flow_ratio", 2.}, { "extrusion_width", 0.4 } });
THEN("Output flow is as expected.") {
REQUIRE(test(config));
}
}
}

View file

@ -0,0 +1,60 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Gaps", "[Gaps]") {
GIVEN("Two hollow squares") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeter_speed", 66 },
{ "external_perimeter_speed", 66 },
{ "small_perimeter_speed", 66 },
{ "gap_fill_speed", 99 },
{ "perimeters", 1 },
// to prevent speeds from being altered
{ "cooling", 0 },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
{ "perimeter_extrusion_width", 0.35 },
{ "first_layer_extrusion_width", 0.35 }
});
GCodeReader parser;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60;
const double gap_fill_speed = config.opt_float("gap_fill_speed") * 60;
std::string last; // perimeter or gap
Points perimeter_points;
int gap_fills_outside_last_perimeters = 0;
parser.parse_buffer(
Slic3r::Test::slice({ Slic3r::Test::TestMesh::two_hollow_squares }, config),
[&perimeter_points, &gap_fills_outside_last_perimeters, &last, perimeter_speed, gap_fill_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
Point point = line.new_XY_scaled(self);
if (is_approx(f, perimeter_speed)) {
if (last == "gap")
perimeter_points.clear();
perimeter_points.emplace_back(point);
last = "perimeter";
} else if (is_approx(f, gap_fill_speed)) {
Polygon convex_hull = Geometry::convex_hull(perimeter_points);
if (! convex_hull.contains(point))
++ gap_fills_outside_last_perimeters;
last = "gap";
}
}
});
THEN("gap fills are printed before leaving islands") {
REQUIRE(gap_fills_outside_last_perimeters == 0);
}
}
}

View file

@ -0,0 +1,191 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
SCENARIO("Medial Axis", "[ThinWalls]") {
GIVEN("Square with hole") {
auto square = Polygon::new_scale({ {100, 100}, {200, 100}, {200, 200}, {100, 200} });
auto hole_in_square = Polygon::new_scale({ {140, 140}, {140, 160}, {160, 160}, {160, 140} });
ExPolygon expolygon{ square, hole_in_square };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(40.), scaled<double>(0.5));
THEN("medial axis of a square shape is a single path") {
REQUIRE(res.size() == 1);
}
THEN("polyline forms a closed loop") {
REQUIRE(res.front().first_point() == res.front().last_point());
}
THEN("medial axis loop has reasonable length") {
REQUIRE(res.front().length() > hole_in_square.length());
REQUIRE(res.front().length() < square.length());
}
}
}
GIVEN("narrow rectangle") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 200}, {100, 200} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
THEN("medial axis of a narrow rectangle is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
#if 0
//FIXME this test never worked
GIVEN("narrow rectangle with an extra vertex") {
ExPolygon expolygon{ Polygon::new_scale({
{100, 100}, {120, 100}, {120, 200},
{105, 200} /* extra point in the short side*/,
{100, 200}
})};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(1.), scaled<double>(0.5));
THEN("medial axis of a narrow rectangle with an extra vertex is still a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has still a reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
THEN("extra vertices don't influence medial axis") {
size_t invalid = 0;
for (const Polyline &pl : res)
for (const Point &p : pl.points)
if (std::abs(p.y() - scaled<coord_t>(150.)) < SCALED_EPSILON)
++ invalid;
REQUIRE(invalid == 0);
}
}
}
#endif
GIVEN("semicircumference") {
ExPolygon expolygon{{
{1185881,829367},{1421988,1578184},{1722442,2303558},{2084981,2999998},{2506843,3662186},{2984809,4285086},{3515250,4863959},{4094122,5394400},
{4717018,5872368},{5379210,6294226},{6075653,6656769},{6801033,6957229},{7549842,7193328},{8316383,7363266},{9094809,7465751},{9879211,7500000},
{10663611,7465750},{11442038,7363265},{12208580,7193327},{12957389,6957228},{13682769,6656768},{14379209,6294227},{15041405,5872366},
{15664297,5394401},{16243171,4863960},{16758641,4301424},{17251579,3662185},{17673439,3000000},{18035980,2303556},{18336441,1578177},
{18572539,829368},{18750748,0},{19758422,0},{19727293,236479},{19538467,1088188},{19276136,1920196},{18942292,2726179},{18539460,3499999},
{18070731,4235755},{17539650,4927877},{16950279,5571067},{16307090,6160437},{15614974,6691519},{14879209,7160248},{14105392,7563079},
{13299407,7896927},{12467399,8159255},{11615691,8348082},{10750769,8461952},{9879211,8500000},{9007652,8461952},{8142729,8348082},
{7291022,8159255},{6459015,7896927},{5653029,7563079},{4879210,7160247},{4143447,6691519},{3451331,6160437},{2808141,5571066},{2218773,4927878},
{1687689,4235755},{1218962,3499999},{827499,2748020},{482284,1920196},{219954,1088186},{31126,236479},{0,0},{1005754,0}
}};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(1.324888), scaled<double>(0.25));
THEN("medial axis of a semicircumference is a single line") {
REQUIRE(res.size() == 1);
}
THEN("all medial axis segments of a semicircumference have the same orientation") {
int nccw = 0;
int ncw = 0;
for (const Polyline &pl : res)
for (size_t i = 1; i + 1 < pl.size(); ++ i) {
double cross = cross2((pl.points[i] - pl.points[i - 1]).cast<double>(), (pl.points[i + 1] - pl.points[i]).cast<double>());
if (cross > 0.)
++ nccw;
else if (cross < 0.)
++ ncw;
}
REQUIRE((ncw == 0 || nccw == 0));
}
}
}
GIVEN("narrow trapezoid") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {112, 200}, {108, 200} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
THEN("medial axis of a narrow trapezoid is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
GIVEN("L shape") {
ExPolygon expolygon{ Polygon::new_scale({ {100, 100}, {120, 100}, {120, 180}, {200, 180}, {200, 200}, {100, 200}, }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(20.), scaled<double>(0.5));
THEN("medial axis of an L shape is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
// 20 is the thickness of the expolygon, which is subtracted from the ends
auto len = unscale<double>(res.front().length()) + 20;
REQUIRE(len > 80. * 2.);
REQUIRE(len < 100. * 2.);
}
}
}
GIVEN("whatever shape") {
ExPolygon expolygon{{
{-203064906,-51459966},{-219312231,-51459966},{-219335477,-51459962},{-219376095,-51459962},{-219412047,-51459966},
{-219572094,-51459966},{-219624814,-51459962},{-219642183,-51459962},{-219656665,-51459966},{-220815482,-51459966},
{-220815482,-37738966},{-221117540,-37738966},{-221117540,-51762024},{-203064906,-51762024},
}};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(819998., 102499.75);
THEN("medial axis is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
double perimeter = expolygon.contour.split_at_first_point().length();
REQUIRE(total_length(res) > perimeter / 2. / 4. * 3.);
}
}
}
GIVEN("narrow triangle") {
ExPolygon expolygon{ Polygon::new_scale({ {50, 100}, {1000, 102}, {50, 104} }) };
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(scaled<double>(4.), scaled<double>(0.5));
THEN("medial axis of a narrow triangle is a single line") {
REQUIRE(res.size() == 1);
}
THEN("medial axis has reasonable length") {
REQUIRE(res.front().length() >= scaled<double>(200.-100. - (120.-100.)) - SCALED_EPSILON);
}
}
}
GIVEN("GH #2474") {
ExPolygon expolygon{{ {91294454,31032190},{11294481,31032190},{11294481,29967810},{44969182,29967810},{89909960,29967808},{91294454,29967808} }};
WHEN("Medial axis is extracted") {
Polylines res = expolygon.medial_axis(1871238, 500000);
THEN("medial axis is a single line") {
REQUIRE(res.size() == 1);
}
Polyline &polyline = res.front();
THEN("medial axis is horizontal and is centered") {
double expected_y = expolygon.contour.bounding_box().center().y();
double center_y = 0.;
for (auto &p : polyline.points)
center_y += double(p.y());
REQUIRE(std::abs(center_y / polyline.size() - expected_y) < SCALED_EPSILON);
}
// order polyline from left to right
if (polyline.first_point().x() > polyline.last_point().x())
polyline.reverse();
BoundingBox polyline_bb = polyline.bounding_box();
THEN("expected x_min") {
REQUIRE(polyline.first_point().x() == polyline_bb.min.x());
}
THEN("expected x_max") {
REQUIRE(polyline.last_point().x() == polyline_bb.max.x());
}
THEN("medial axis is monotonous in x (not self intersecting)") {
Polyline sorted { polyline };
std::sort(sorted.begin(), sorted.end());
REQUIRE(polyline == sorted);
}
}
}
}

View file

@ -13,6 +13,7 @@ add_executable(${_TEST_NAME}_tests
test_geometry.cpp
test_placeholder_parser.cpp
test_polygon.cpp
test_polyline.cpp
test_mutable_polygon.cpp
test_mutable_priority_queue.cpp
test_stl.cpp

View file

@ -1,5 +1,6 @@
#include <catch2/catch.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/LocalesUtils.hpp"
@ -13,20 +14,20 @@ using namespace Slic3r;
SCENARIO("Generic config validation performs as expected.", "[Config]") {
GIVEN("A config generated from default options") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
WHEN( "perimeter_extrusion_width is set to 250%, a valid value") {
WHEN("perimeter_extrusion_width is set to 250%, a valid value") {
config.set_deserialize_strict("perimeter_extrusion_width", "250%");
THEN( "The config is read as valid.") {
REQUIRE(config.validate().empty());
}
}
WHEN( "perimeter_extrusion_width is set to -10, an invalid value") {
WHEN("perimeter_extrusion_width is set to -10, an invalid value") {
config.set("perimeter_extrusion_width", -10);
THEN( "Validate returns error") {
REQUIRE(! config.validate().empty());
}
}
WHEN( "perimeters is set to -10, an invalid value") {
WHEN("perimeters is set to -10, an invalid value") {
config.set("perimeters", -10);
THEN( "Validate returns error") {
REQUIRE(! config.validate().empty());
@ -36,8 +37,7 @@ SCENARIO("Generic config validation performs as expected.", "[Config]") {
}
SCENARIO("Config accessor functions perform as expected.", "[Config]") {
GIVEN("A config generated from default options") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
auto test = [](ConfigBase &config) {
WHEN("A boolean option is set to a boolean value") {
REQUIRE_NOTHROW(config.set("gcode_comments", true));
THEN("The underlying value is set correctly.") {
@ -70,8 +70,8 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") {
}
}
#if 0
//FIXME better design accessors for vector elements.
WHEN("An integer-based option is set through the integer interface") {
//FIXME better design accessors for vector elements.
WHEN("An integer-based option is set through the integer interface") {
config.set("bed_temperature", 100);
THEN("The underlying value is set correctly.") {
REQUIRE(config.opt<ConfigOptionInts>("bed_temperature")->get_at(0) == 100);
@ -193,6 +193,14 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") {
REQUIRE(config.opt_float("layer_height") == 0.5);
}
}
};
GIVEN("DynamicPrintConfig generated from default options") {
auto config = Slic3r::DynamicPrintConfig::full_print_config();
test(config);
}
GIVEN("FullPrintConfig generated from default options") {
Slic3r::FullPrintConfig config;
test(config);
}
}

View file

@ -162,14 +162,34 @@ TEST_CASE("Splitting a Polygon generates a polyline correctly", "[Geometry]"){
}
TEST_CASE("Bounding boxes are scaled appropriately", "[Geometry]"){
BoundingBox bb(std::vector<Point>({Point(0, 1), Point(10, 2), Point(20, 2)}));
bb.scale(2);
REQUIRE(bb.min == Point(0,2));
REQUIRE(bb.max == Point(40,4));
SCENARIO("BoundingBox", "[Geometry]") {
WHEN("Bounding boxes are scaled") {
BoundingBox bb(std::vector<Point>({Point(0, 1), Point(10, 2), Point(20, 2)}));
bb.scale(2);
REQUIRE(bb.min == Point(0,2));
REQUIRE(bb.max == Point(40,4));
}
WHEN("BoundingBox constructed from points") {
BoundingBox bb(Points{ {100,200}, {100, 200}, {500, -600} });
THEN("minimum is correct") {
REQUIRE(bb.min == Point{100,-600});
}
THEN("maximum is correct") {
REQUIRE(bb.max == Point{500,200});
}
}
WHEN("BoundingBox constructed from a single point") {
BoundingBox bb;
bb.merge({10, 10});
THEN("minimum equals to the only defined point") {
REQUIRE(bb.min == Point{10,10});
}
THEN("maximum equals to the only defined point") {
REQUIRE(bb.max == Point{10,10});
}
}
}
TEST_CASE("Offseting a line generates a polygon correctly", "[Geometry]"){
Slic3r::Polyline tmp = { Point(10,10), Point(20,10) };
Slic3r::Polygon area = offset(tmp,5).at(0);

View file

@ -148,3 +148,65 @@ SCENARIO("Remove collinear points from Polygon", "[Polygon]") {
}
}
}
SCENARIO("Simplify polygon", "[Polygon]")
{
GIVEN("gear") {
auto gear = Polygon::new_scale({
{144.9694,317.1543}, {145.4181,301.5633}, {146.3466,296.921}, {131.8436,294.1643}, {131.7467,294.1464},
{121.7238,291.5082}, {117.1631,290.2776}, {107.9198,308.2068}, {100.1735,304.5101}, {104.9896,290.3672},
{106.6511,286.2133}, {93.453,279.2327}, {81.0065,271.4171}, {67.7886,286.5055}, {60.7927,280.1127},
{69.3928,268.2566}, {72.7271,264.9224}, {61.8152,253.9959}, {52.2273,242.8494}, {47.5799,245.7224},
{34.6577,252.6559}, {30.3369,245.2236}, {42.1712,236.3251}, {46.1122,233.9605}, {43.2099,228.4876},
{35.0862,211.5672}, {33.1441,207.0856}, {13.3923,212.1895}, {10.6572,203.3273}, {6.0707,204.8561},
{7.2775,204.4259}, {29.6713,196.3631}, {25.9815,172.1277}, {25.4589,167.2745}, {19.8337,167.0129},
{5.0625,166.3346}, {5.0625,156.9425}, {5.3701,156.9282}, {21.8636,156.1628}, {25.3713,156.4613},
{25.4243,155.9976}, {29.3432,155.8157}, {30.3838,149.3549}, {26.3596,147.8137}, {27.1085,141.2604},
{29.8466,126.8337}, {24.5841,124.9201}, {10.6664,119.8989}, {13.4454,110.9264}, {33.1886,116.0691},
{38.817,103.1819}, {45.8311,89.8133}, {30.4286,76.81}, {35.7686,70.0812}, {48.0879,77.6873},
{51.564,81.1635}, {61.9006,69.1791}, {72.3019,58.7916}, {60.5509,42.5416}, {68.3369,37.1532},
{77.9524,48.1338}, {80.405,52.2215}, {92.5632,44.5992}, {93.0123,44.3223}, {106.3561,37.2056},
{100.8631,17.4679}, {108.759,14.3778}, {107.3148,11.1283}, {117.0002,32.8627}, {140.9109,27.3974},
{145.7004,26.4994}, {145.1346,6.1011}, {154.502,5.4063}, {156.9398,25.6501}, {171.0557,26.2017},
{181.3139,27.323}, {186.2377,27.8532}, {191.6031,8.5474}, {200.6724,11.2756}, {197.2362,30.2334},
{220.0789,39.1906}, {224.3261,41.031}, {236.3506,24.4291}, {243.6897,28.6723}, {234.2956,46.7747},
{245.6562,55.1643}, {257.2523,65.0901}, {261.4374,61.5679}, {273.1709,52.8031}, {278.555,59.5164},
{268.4334,69.8001}, {264.1615,72.3633}, {268.2763,77.9442}, {278.8488,93.5305}, {281.4596,97.6332},
{286.4487,95.5191}, {300.2821,90.5903}, {303.4456,98.5849}, {286.4523,107.7253}, {293.7063,131.1779},
{294.9748,135.8787}, {314.918,133.8172}, {315.6941,143.2589}, {300.9234,146.1746}, {296.6419,147.0309},
{297.1839,161.7052}, {296.6136,176.3942}, {302.1147,177.4857}, {316.603,180.3608}, {317.1658,176.7341},
{315.215,189.6589}, {315.1749,189.6548}, {294.9411,187.5222}, {291.13,201.7233}, {286.2615,215.5916},
{291.1944,218.2545}, {303.9158,225.1271}, {299.2384,233.3694}, {285.7165,227.6001}, {281.7091,225.1956},
{273.8981,237.6457}, {268.3486,245.2248}, {267.4538,246.4414}, {264.8496,250.0221}, {268.6392,253.896},
{278.5017,265.2131}, {272.721,271.4403}, {257.2776,258.3579}, {234.4345,276.5687}, {242.6222,294.8315},
{234.9061,298.5798}, {227.0321,286.2841}, {225.2505,281.8301}, {211.5387,287.8187}, {202.3025,291.0935},
{197.307,292.831}, {199.808,313.1906}, {191.5298,315.0787}, {187.3082,299.8172}, {186.4201,295.3766},
{180.595,296.0487}, {161.7854,297.4248}, {156.8058,297.6214}, {154.3395,317.8592}
});
WHEN("simplified") {
size_t num_points = gear.size();
Polygons simplified = gear.simplify(1000.);
THEN("gear simplified to a single polygon") {
REQUIRE(simplified.size() == 1);
}
THEN("gear was reduced using Douglas-Peucker") {
//note printf "original points: %d\nnew points: %d", $num_points, scalar(@{$simplified->[0]});
REQUIRE(simplified.front().size() < num_points);
}
}
}
GIVEN("hole in square") {
// CW oriented
auto hole_in_square = Polygon{ {140, 140}, {140, 160}, {160, 160}, {160, 140} };
WHEN("simplified") {
Polygons simplified = hole_in_square.simplify(2.);
THEN("hole simplification returns one polygon") {
REQUIRE(simplified.size() == 1);
}
THEN("hole simplification turns cw polygon into ccw polygon") {
REQUIRE(simplified.front().is_counter_clockwise());
}
}
}
}

View file

@ -0,0 +1,28 @@
#include <catch2/catch.hpp>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
using namespace Slic3r;
SCENARIO("Simplify polyline", "[Polyline]")
{
GIVEN("polyline 1") {
auto polyline = Polyline{ {0,0},{1,0},{2,0},{2,1},{2,2},{1,2},{0,2},{0,1},{0,0} };
WHEN("simplified with Douglas-Peucker") {
polyline.simplify(1.);
THEN("simplified correctly") {
REQUIRE(polyline == Polyline{ {0,0}, {2,0}, {2,2}, {0,2}, {0,0} });
}
}
}
GIVEN("polyline 2") {
auto polyline = Polyline{ {0,0}, {50,50}, {100,0}, {125,-25}, {150,50} };
WHEN("simplified with Douglas-Peucker") {
polyline.simplify(25.);
THEN("not simplified") {
REQUIRE(polyline == Polyline{ {0,0}, {50,50}, {125,-25}, {150,50} });
}
}
}
}