1 //Copyright (c) 2019 Ultimaker B.V.
2 //CuraEngine is released under the terms of the AGPLv3 or higher.
3 
4 #include <gtest/gtest.h>
5 
6 #include "../src/Application.h" //To set up a scene and load settings that the layer plan and merger need.
7 #include "../src/FanSpeedLayerTime.h" //Required to construct a layer plan. Doesn't influence our tests.
8 #include "../src/pathPlanning/GCodePath.h" //The paths that we're going to be merging.
9 #include "../src/LayerPlan.h" //Constructing plans that the mergers can merge lines in.
10 #include "../src/MergeInfillLines.h" //The class under test.
11 #include "../src/RetractionConfig.h" //Required to construct a layer plan. Doesn't influence our tests.
12 #include "../src/Slice.h" //To set up a scene and load settings that the layer plan and merger need.
13 #include "../src/settings/types/LayerIndex.h" //Required to construct a layer plan. Doesn't influence our tests.
14 
15 namespace cura
16 {
17 
18 class MergeInfillLinesTest : public testing::Test
19 {
20 public:
21     //These settings don't matter for this test so they are all the same for every fixture.
22     const size_t extruder_nr = 0;
23     const LayerIndex layer_nr = 0;
24     const bool is_initial_layer = false;
25     const bool is_raft_layer = false;
26     const coord_t layer_thickness = 100;
27     const Point starting_position; //All plans start at 0,0.
28 
29     /*
30      * A merger to test with.
31      */
32     ExtruderPlan* extruder_plan;
33     MergeInfillLines* merger;
34 
35     /*
36      * These fields are required for constructing layer plans and must be held
37      * constant for as long as the lifetime of the plans. Construct them once
38      * and store them in this fixture class.
39      */
40     const FanSpeedLayerTimeSettings fan_speed_layer_time;
41     const RetractionConfig retraction_config;
42     const GCodePathConfig skin_config;
43     const GCodePathConfig travel_config;
44 
45     /*
46      * A path of skin lines without any points.
47      */
48     GCodePath empty_skin;
49 
50     /*
51      * A path of a single skin line.
52      */
53     GCodePath single_skin;
54 
55     /*
56      * A path of multiple skin lines that together form a straight line.
57      *
58      * This path should not get merged together to a single line.
59      */
60     GCodePath lengthwise_skin;
61 
62     /*
63      * Basic zigzag of skin lines.
64      * There are 11 lines with travel moves in between them. The lines are 100
65      * microns long and 400 microns wide. They should get merged to one long
66      * line of 100 microns wide and 4400 microns long.
67      */
68     std::vector<GCodePath> zigzag;
69 
MergeInfillLinesTest()70     MergeInfillLinesTest()
71      : starting_position(0, 0)
72      , fan_speed_layer_time()
73      , retraction_config()
74      , skin_config(PrintFeatureType::Skin, 400, layer_thickness, 1, GCodePathConfig::SpeedDerivatives{50, 1000, 10})
75      , travel_config(PrintFeatureType::MoveCombing, 0, layer_thickness, 0, GCodePathConfig::SpeedDerivatives{100, 1000, 10})
76      , empty_skin(skin_config, "merge_infill_lines_mesh", SpaceFillType::None, 1.0, false)
77      , single_skin(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, 1.0, false)
78      , lengthwise_skin(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, 1.0, false)
79     {
80         single_skin.points.emplace_back(1000, 0);
81 
82         lengthwise_skin.points = {Point(1000, 0),
83                                   Point(2000, 0),
84                                   Point(3000, 0),
85                                   Point(4000, 0)};
86 
87         //Create the zigzag.
88         constexpr Ratio normal_flow = 1.0;
89         constexpr bool no_spiralize = false;
90         constexpr size_t num_lines = 10;
91         //Creates a zig-zag line with extrusion moves when moving in the Y direction and travel moves in between:
92         //  _   _   _
93         // | |_| |_| |_|
94         for(size_t i = 0; i < num_lines; i++)
95         {
96             zigzag.emplace_back(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, normal_flow, no_spiralize);
97             zigzag.back().points.emplace_back(400 * i, 100 * ((i + 1) % 2));
98             zigzag.emplace_back(travel_config, "merge_infill_lines_mesh", SpaceFillType::None, normal_flow, no_spiralize);
99             zigzag.back().points.emplace_back(400 * (i + 1), 100 * ((i + 1) % 2));
100         }
101         //End with an extrusion move, not a travel move.
102         zigzag.emplace_back(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, normal_flow, no_spiralize);
103         zigzag.back().points.emplace_back(400 * num_lines, 100 * ((num_lines + 1) % 2));
104     }
105 
SetUp()106     void SetUp()
107     {
108         //Set up a scene so that we may request settings.
109         Application::getInstance().current_slice = new Slice(1);
110         Application::getInstance().current_slice->scene.extruders.emplace_back(0, nullptr);
111         ExtruderTrain& train = Application::getInstance().current_slice->scene.extruders.back();
112         train.settings.add("machine_nozzle_size", "0.4");
113         train.settings.add("meshfix_maximum_deviation", "0.1");
114 
115         extruder_plan = new ExtruderPlan(extruder_nr, layer_nr, is_initial_layer, is_raft_layer, layer_thickness, fan_speed_layer_time, retraction_config);
116         merger = new MergeInfillLines(*extruder_plan);
117     }
118 
TearDown()119     void TearDown()
120     {
121         delete extruder_plan;
122         delete merger;
123         delete Application::getInstance().current_slice;
124     }
125 };
126 
TEST_F(MergeInfillLinesTest,CalcPathLengthEmpty)127 TEST_F(MergeInfillLinesTest, CalcPathLengthEmpty)
128 {
129     EXPECT_EQ(0, merger->calcPathLength(starting_position, empty_skin));
130 }
131 
TEST_F(MergeInfillLinesTest,CalcPathLengthSingle)132 TEST_F(MergeInfillLinesTest, CalcPathLengthSingle)
133 {
134     EXPECT_EQ(1000, merger->calcPathLength(starting_position, single_skin));
135 }
136 
TEST_F(MergeInfillLinesTest,CalcPathLengthMultiple)137 TEST_F(MergeInfillLinesTest, CalcPathLengthMultiple)
138 {
139     EXPECT_EQ(4000, merger->calcPathLength(starting_position, lengthwise_skin));
140 }
141 
142 /*
143  * Tries merging an empty set of paths together.
144  *
145  * This changes nothing in the paths, since there is nothing to change.
146  */
TEST_F(MergeInfillLinesTest,MergeEmpty)147 TEST_F(MergeInfillLinesTest, MergeEmpty)
148 {
149     std::vector<GCodePath> paths; //Empty. No paths to merge.
150 
151     const bool result = merger->mergeInfillLines(paths, starting_position);
152 
153     EXPECT_FALSE(result) << "There are no lines to merge.";
154     EXPECT_EQ(paths.size(), 0) << "The number of paths should still be zero.";
155 }
156 
157 /*
158  * Tries merging a single path of a single line.
159  *
160  * This changes nothing in the paths, since the line cannot be merged with
161  * anything else.
162  */
TEST_F(MergeInfillLinesTest,MergeSingle)163 TEST_F(MergeInfillLinesTest, MergeSingle)
164 {
165     std::vector<GCodePath> paths;
166     paths.push_back(single_skin);
167 
168     const bool result = merger->mergeInfillLines(paths, starting_position);
169 
170     EXPECT_FALSE(result) << "There is only one line, so it can't be merged with other lines.";
171     ASSERT_EQ(paths.size(), 1) << "The path should not get removed.";
172     EXPECT_EQ(paths[0].points.size(), 1) << "The path should not be modified.";
173 }
174 
175 /*
176  * Tries merging a single path that consists of multiple vertices in a straight
177  * line.
178  *
179  * This should not change anything in the paths, since the lines are in a single
180  * path without travel moves in between. It's just drawing a curve, and that
181  * curve should not get modified.
182  *
183  * This is basically the case that went wrong with the "Weird Fat Infill" bug
184  * (CURA-5776).
185  */
TEST_F(MergeInfillLinesTest,MergeLenthwise)186 TEST_F(MergeInfillLinesTest, MergeLenthwise)
187 {
188     std::vector<GCodePath> paths;
189     paths.push_back(lengthwise_skin);
190 
191     const bool result = merger->mergeInfillLines(paths, starting_position);
192 
193     EXPECT_FALSE(result) << "Patterns like Gyroid infill with many (almost) lengthwise lines should not get merged, even if those lines are short.";
194     ASSERT_EQ(paths.size(), 1) << "The path should not get removed or split.";
195     EXPECT_EQ(paths[0].points.size(), 4) << "The path should not be modified.";
196 }
197 
198 /*
199  * Tries merging a bunch of parallel lines with travel moves in between.
200  *
201  * This is the basic use case for merging infill lines.
202  */
TEST_F(MergeInfillLinesTest,MergeParallel)203 TEST_F(MergeInfillLinesTest, MergeParallel)
204 {
205     const bool result = merger->mergeInfillLines(zigzag, starting_position);
206 
207     EXPECT_TRUE(result) << "The simple zig-zag pattern should get merged fine.";
208     EXPECT_LE(zigzag.size(), 5); //Some lenience. Ideally it'd be one.
209 }
210 
211 /*
212  * Tests if the total extruded volume is the same as the original lines.
213  */
TEST_F(MergeInfillLinesTest,DISABLED_ExtrudedVolume)214 TEST_F(MergeInfillLinesTest, DISABLED_ExtrudedVolume)
215 {
216     coord_t original_volume = 0;
217     Point position = starting_position;
218     for(const GCodePath& path : zigzag)
219     {
220         for(const Point& point : path.points)
221         {
222             const coord_t length = vSize(point - position);
223             original_volume += length * (path.getExtrusionMM3perMM() * 1000000);
224             position = point;
225         }
226     }
227 
228     merger->mergeInfillLines(zigzag, starting_position);
229     /* If it fails to merge, other tests fail. This test depends on that, but we
230     don't necessarily want it to fail as a false negative if merging in general
231     fails, because we don't want to think that the volume is wrong then. So we
232     don't check the outcome of the merging itself, just the volume. */
233 
234     coord_t new_volume = 0;
235     position = starting_position;
236     for(const GCodePath& path : zigzag)
237     {
238         for(const Point& point : path.points)
239         {
240             const coord_t length = vSize(point - position);
241             new_volume += length * (path.getExtrusionMM3perMM() * 1000000);
242             position = point;
243         }
244     }
245 
246     EXPECT_EQ(original_volume, new_volume);
247 }
248 
249 /*
250  * Parameterised test for merging infill lines inside thin walls.
251  *
252  * The thin walls are filled with lines of various rotations. This is the float
253  * parameter.
254  */
255 class MergeInfillLinesThinWallsTest : public MergeInfillLinesTest, public testing::WithParamInterface<double>
256 {
257 public:
258     const coord_t wall_thickness = 200; //0.2mm wall.
259 };
260 
TEST_P(MergeInfillLinesThinWallsTest,DISABLED_MergeThinWalls)261 TEST_P(MergeInfillLinesThinWallsTest, DISABLED_MergeThinWalls)
262 {
263     const AngleRadians rotation = AngleRadians(GetParam()); //Converts degrees to radians!
264     constexpr Ratio normal_flow = 1.0;
265     constexpr bool no_spiralize = false;
266     constexpr size_t num_lines = 10;
267 
268     //Construct parallel lines under a certain rotation. It looks like this: /////
269     //The perpendicular distance between the lines will be exactly one line width.
270     //The distance between the adjacent endpoints of the lines will be greater for steeper angles.
271     std::vector<GCodePath> paths;
272     const coord_t line_shift = wall_thickness / std::cos(rotation); //How far the top of the line is shifted from the bottom.
273     const coord_t line_horizontal_spacing = skin_config.getLineWidth() / std::sin(rotation); //Horizontal spacing between starting vertices.
274     for(size_t i = 0; i < num_lines; i++)
275     {
276         paths.emplace_back(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, normal_flow, no_spiralize);
277         paths.back().points.emplace_back(line_horizontal_spacing * i + line_shift * ((i + 1) % 2), wall_thickness * ((i + 1) % 2));
278         paths.emplace_back(travel_config, "merge_infill_lines_mesh", SpaceFillType::None, normal_flow, no_spiralize);
279         paths.back().points.emplace_back(line_horizontal_spacing * (i + 1) + line_shift * ((i + 1) % 2), wall_thickness * ((i + 1) % 2));
280     }
281     paths.emplace_back(skin_config, "merge_infill_lines_mesh", SpaceFillType::Lines, normal_flow, no_spiralize);
282     paths.back().points.emplace_back(line_horizontal_spacing * num_lines + line_shift * ((num_lines + 1) % 2), wall_thickness * ((num_lines + 1) % 2));
283 
284     const bool merged = merger->mergeInfillLines(paths, starting_position);
285 
286     EXPECT_TRUE(merged) << "These are the test cases where line segments should get merged.";
287     EXPECT_LE(paths.size(), 5) << "Should get merged to 1 line, but give a bit of leeway.";
288 }
289 
290 INSTANTIATE_TEST_CASE_P(MergeThinWallsTest, MergeInfillLinesThinWallsTest, testing::Range(-45.0, 45.0, 5.0));
291 
292 } //namespace cura