QIDISlicer1.0.0

This commit is contained in:
sunsets
2023-06-10 10:14:12 +08:00
parent f2e20e1a90
commit b4cd486f2d
3475 changed files with 1973675 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_avoid_crossing_perimeters.cpp
test_bridges.cpp
test_cooling.cpp
test_clipper.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
test_model.cpp
test_multi.cpp
test_perimeters.cpp
test_print.cpp
test_printgcode.cpp
test_printobject.cpp
test_shells.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)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
if (WIN32)
qidislicer_copy_dlls(${_TEST_NAME}_tests)
endif()
# catch_discover_tests(${_TEST_NAME}_tests TEST_PREFIX "${_TEST_NAME}: ")
add_test(${_TEST_NAME}_tests ${_TEST_NAME}_tests ${CATCH_EXTRA_ARGS})

View File

@@ -0,0 +1,18 @@
#include <catch_main.hpp>
#include "libslic3r/libslic3r.h"
// __has_feature() is used later for Clang, this is for compatibility with other compilers (such as GCC and MSVC)
#ifndef __has_feature
# define __has_feature(x) 0
#endif
// Print reports about memory leaks but exit with zero exit code when any memory leaks is found to make unit tests pass.
// After merging the stable branch (2.4.1) with the master branch, this should be deleted.
#if __has_feature(address_sanitizer) || defined(__SANITIZE_ADDRESS__)
extern "C" {
const char *__lsan_default_options() {
return "exitcode=0";
}
}
#endif

View File

@@ -0,0 +1,16 @@
#include <catch2/catch.hpp>
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Avoid crossing perimeters", "[AvoidCrossingPerimeters]") {
WHEN("Two 20mm cubes sliced") {
std::string gcode = Slic3r::Test::slice(
{ Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20 },
{ { "avoid_crossing_perimeters", true } });
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
}
}

View File

@@ -0,0 +1,133 @@
#include <catch2/catch.hpp>
#include <libslic3r/BridgeDetector.hpp>
#include <libslic3r/Geometry.hpp>
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Bridge detector", "[Bridging]")
{
auto check_angle = [](const ExPolygons &lower, const ExPolygon &bridge, double expected, double tolerance = -1, double expected_coverage = -1)
{
if (expected_coverage < 0)
expected_coverage = bridge.area();
BridgeDetector bridge_detector(bridge, lower, scaled<coord_t>(0.5)); // extrusion width
if (tolerance < 0)
tolerance = Geometry::rad2deg(bridge_detector.resolution) + EPSILON;
bridge_detector.detect_angle();
double result = bridge_detector.angle;
Polygons coverage = bridge_detector.coverage();
THEN("correct coverage area") {
REQUIRE(is_approx(area(coverage), expected_coverage));
}
// our epsilon is equal to the steps used by the bridge detection algorithm
//##use XXX; YYY [ rad2deg($result), $expected ];
// returned value must be non-negative, check for that too
double delta = Geometry::rad2deg(result) - expected;
if (delta >= 180. - EPSILON)
delta -= 180;
return result >= 0. && std::abs(delta) < tolerance;
};
GIVEN("O-shaped overhang") {
auto test = [&check_angle](const Point &size, double rotate, double expected_angle, double tolerance = -1) {
ExPolygon lower{
Polygon::new_scale({ {-2,-2}, {size.x()+2,-2}, {size.x()+2,size.y()+2}, {-2,size.y()+2} }),
Polygon::new_scale({ {0,0}, {0,size.y()}, {size.x(),size.y()}, {size.x(),0} } )
};
lower.rotate(Geometry::deg2rad(rotate), size / 2);
ExPolygon bridge_expoly(lower.holes.front());
bridge_expoly.contour.reverse();
return check_angle({ lower }, bridge_expoly, expected_angle, tolerance);
};
WHEN("Bridge size 20x10") {
bool valid = test({20,10}, 0., 90.);
THEN("bridging angle is 90 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 10x20") {
bool valid = test({10,20}, 0., 0.);
THEN("bridging angle is 0 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 20x10, rotated by 45 degrees") {
bool valid = test({20,10}, 45., 135., 20.);
THEN("bridging angle is 135 degrees") {
REQUIRE(valid);
}
}
WHEN("Bridge size 20x10, rotated by 135 degrees") {
bool valid = test({20,10}, 135., 45., 20.);
THEN("bridging angle is 45 degrees") {
REQUIRE(valid);
}
}
}
GIVEN("two-sided bridge") {
ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {20,10}, {0,10} }) };
ExPolygons lower { ExPolygon{ Polygon::new_scale({ {-2,0}, {0,0}, {0,10}, {-2,10} }) } };
lower.emplace_back(lower.front());
lower.back().translate(Point::new_scale(22, 0));
THEN("Bridging angle 0 degrees") {
REQUIRE(check_angle(lower, bridge, 0));
}
}
GIVEN("for C-shaped overhang") {
ExPolygon bridge{ Polygon::new_scale({ {0,0}, {20,0}, {10,10}, {0,10} }) };
ExPolygon lower{ Polygon::new_scale({ {0,0}, {0,10}, {10,10}, {10,12}, {-2,12}, {-2,-2}, {22,-2}, {22,0} }) };
bool valid = check_angle({ lower }, bridge, 135);
THEN("Bridging angle is 135 degrees") {
REQUIRE(valid);
}
}
GIVEN("square overhang with L-shaped anchors") {
ExPolygon bridge{ Polygon::new_scale({ {10,10}, {20,10}, {20,20}, {10,20} }) };
ExPolygon lower{ Polygon::new_scale({ {10,10}, {10,20}, {20,20}, {30,30}, {0,30}, {0,0} }) };
bool valid = check_angle({ lower }, bridge, 45., -1., bridge.area() / 2.);
THEN("Bridging angle is 45 degrees") {
REQUIRE(valid);
}
}
}
SCENARIO("Bridging integration", "[Bridging]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "top_solid_layers", 0 },
// to prevent bridging on sparse infill
{ "bridge_speed", 99 }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::bridge }, config);
GCodeReader parser;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
// angle => length
std::map<coord_t, double> extrusions;
parser.parse_buffer(gcode, [&extrusions, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (line.cmd() == "G1" && is_approx<double>(bridge_speed, line.new_F(self))) {
// Accumulate lengths of bridging extrusions according to bridging angle.
Line l{ self.xy_scaled(), line.new_XY_scaled(self) };
size_t angle = scaled<coord_t>(l.direction());
auto it = extrusions.find(angle);
if (it == extrusions.end())
it = extrusions.insert(std::make_pair(angle, 0.)).first;
it->second += l.length();
}
});
THEN("bridge is generated") {
REQUIRE(! extrusions.empty());
}
THEN("bridge has the expected direction 0 degrees") {
// Bridging with the longest extrusion.
auto it_longest_extrusion = std::max_element(extrusions.begin(), extrusions.end(),
[](const auto &e1, const auto &e2){ return e1.second < e2.second; });
REQUIRE(it_longest_extrusion->first == 0);
}
}

View File

@@ -0,0 +1,203 @@
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include "libslic3r/ClipperZUtils.hpp"
#include "libslic3r/clipper.hpp"
using namespace Slic3r;
// tests for ExPolygon::overlaps(const ExPolygon &other)
SCENARIO("Clipper intersection with polyline", "[Clipper]")
{
struct TestData {
ClipperLib::Path subject;
ClipperLib::Path clip;
ClipperLib::Paths result;
};
auto run_test = [](const TestData &data) {
ClipperLib::Clipper clipper;
clipper.AddPath(data.subject, ClipperLib::ptSubject, false);
clipper.AddPath(data.clip, ClipperLib::ptClip, true);
ClipperLib::PolyTree polytree;
ClipperLib::Paths paths;
clipper.Execute(ClipperLib::ctIntersection, polytree, ClipperLib::pftNonZero, ClipperLib::pftNonZero);
ClipperLib::PolyTreeToPaths(polytree, paths);
REQUIRE(paths == data.result);
};
WHEN("Open polyline completely inside stays inside") {
run_test({
{ { 10, 0 }, { 20, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 20, 0 }, { 10, 0 } } }
});
};
WHEN("Closed polyline completely inside stays inside") {
run_test({
{ { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 10, 0 }, { 20, 0 }, { 20, 20 }, { 10, 20 }, { 10, 0 } } }
});
};
WHEN("Polyline which crosses right rectangle boundary is trimmed") {
run_test({
{ { 10, 0 }, { 2000, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, 0 }, { 10, 0 } } }
});
};
WHEN("Polyline which is outside clipping region is removed") {
run_test({
{ { 1500, 0 }, { 2000, 0 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
WHEN("Polyline on left vertical boundary is kept") {
run_test({
{ { -1000, -1000 }, { -1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { -1000, -1000 }, { -1000, 1000 } } }
});
run_test({
{ { -1000, 1000 }, { -1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { -1000, 1000 }, { -1000, -1000 } } }
});
};
WHEN("Polyline on right vertical boundary is kept") {
run_test({
{ { 1000, -1000 }, { 1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, -1000 }, { 1000, 1000 } } }
});
run_test({
{ { 1000, 1000 }, { 1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ { { 1000, 1000 }, { 1000, -1000 } } }
});
};
WHEN("Polyline on bottom horizontal boundary is removed") {
run_test({
{ { -1000, -1000 }, { 1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
run_test({
{ { 1000, -1000 }, { -1000, -1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
WHEN("Polyline on top horizontal boundary is removed") {
run_test({
{ { -1000, 1000 }, { 1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
run_test({
{ { 1000, 1000 }, { -1000, 1000 } },
{ { -1000, -1000 }, { -1000, 1000 }, { 1000, 1000 }, { 1000, -1000 } },
{ }
});
};
}
SCENARIO("Clipper Z", "[ClipperZ]")
{
ClipperLib_Z::Path subject { { -2000, -1000, 10 }, { -2000, 1000, 10 }, { 2000, 1000, 10 }, { 2000, -1000, 10 } };
ClipperLib_Z::Path clip{ { -1000, -2000, -5 }, { -1000, 2000, -5 }, { 1000, 2000, -5 }, { 1000, -2000, -5 } };
ClipperLib_Z::Clipper clipper;
clipper.ZFillFunction([](const ClipperLib_Z::IntPoint &e1bot, const ClipperLib_Z::IntPoint &e1top, const ClipperLib_Z::IntPoint &e2bot,
const ClipperLib_Z::IntPoint &e2top, ClipperLib_Z::IntPoint &pt) {
pt.z() = 1;
});
clipper.AddPath(subject, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(polytree, paths);
REQUIRE(paths.size() == 1);
REQUIRE(paths.front().size() == 2);
for (const ClipperLib_Z::IntPoint &pt : paths.front())
REQUIRE(pt.z() == 1);
}
SCENARIO("Intersection with multiple polylines", "[ClipperZ]")
{
// 1000x1000 CCQ square
ClipperLib_Z::Path clip { { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } };
// Two lines interseting inside the square above, crossing the bottom edge of the square.
ClipperLib_Z::Path line1 { { +100, -100, 2 }, { +900, +900, 2 } };
ClipperLib_Z::Path line2 { { +100, +900, 3 }, { +900, -100, 3 } };
ClipperLib_Z::Clipper clipper;
ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections;
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
clipper.ZFillFunction(visitor.clipper_callback());
clipper.AddPath(line1, ClipperLib_Z::ptSubject, false);
clipper.AddPath(line2, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(polytree, paths);
REQUIRE(paths.size() == 2);
THEN("First output polyline is a trimmed 2nd line") {
// Intermediate point (intersection) was removed)
REQUIRE(paths.front().size() == 2);
REQUIRE(paths.front().front().z() == 3);
REQUIRE(paths.front().back().z() < 0);
REQUIRE(intersections[- paths.front().back().z() - 1] == std::pair<coord_t, coord_t>(1, 3));
}
THEN("Second output polyline is a trimmed 1st line") {
// Intermediate point (intersection) was removed)
REQUIRE(paths[1].size() == 2);
REQUIRE(paths[1].front().z() < 0);
REQUIRE(paths[1].back().z() == 2);
REQUIRE(intersections[- paths[1].front().z() - 1] == std::pair<coord_t, coord_t>(1, 2));
}
}
SCENARIO("Interseting a closed loop as an open polyline", "[ClipperZ]")
{
// 1000x1000 CCQ square
ClipperLib_Z::Path clip{ { 0, 0, 1 }, { 1000, 0, 1 }, { 1000, 1000, 1 }, { 0, 1000, 1 } };
// Two lines interseting inside the square above, crossing the bottom edge of the square.
ClipperLib_Z::Path rect{ { 500, 500, 2}, { 500, 1500, 2 }, { 1500, 1500, 2}, { 500, 1500, 2}, { 500, 500, 2 } };
ClipperLib_Z::Clipper clipper;
clipper.AddPath(rect, ClipperLib_Z::ptSubject, false);
clipper.AddPath(clip, ClipperLib_Z::ptClip, true);
ClipperLib_Z::PolyTree polytree;
ClipperLib_Z::Paths paths;
ClipperZUtils::ClipperZIntersectionVisitor::Intersections intersections;
ClipperZUtils::ClipperZIntersectionVisitor visitor(intersections);
clipper.ZFillFunction(visitor.clipper_callback());
clipper.Execute(ClipperLib_Z::ctIntersection, polytree, ClipperLib_Z::pftNonZero, ClipperLib_Z::pftNonZero);
ClipperLib_Z::PolyTreeToPaths(std::move(polytree), paths);
THEN("Open polyline is clipped into two pieces") {
REQUIRE(paths.size() == 2);
REQUIRE(paths.front().size() == 2);
REQUIRE(paths.back().size() == 2);
REQUIRE(paths.front().front().z() == 2);
REQUIRE(paths.back().back().z() == 2);
REQUIRE(paths.front().front().x() == paths.back().back().x());
REQUIRE(paths.front().front().y() == paths.back().back().y());
}
}

View File

@@ -0,0 +1,275 @@
#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 },
{ "enable_dynamic_overhang_speeds", false },
// 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.5;
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,273 @@
#include <catch2/catch.hpp>
#include <exception>
#include <numeric>
#include <sstream>
#include <boost/regex.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Output file format", "[CustomGCode]")
{
WHEN("output_file_format set") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "travel_speed", "130"},
{ "layer_height", "0.4"},
{ "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") {
REQUIRE(output_file == "ts_130_lh_0.4.gcode");
}
}
}
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 (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") {
REQUIRE(num_layer_changes_not_applied == 0);
}
};
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6 } },
{ "extruder", 2 },
{ "first_layer_temperature", { 200, 205 } }
});
config.normalize_fdm();
WHEN("Printing with single but non-zero extruder") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature set correctly for non-zero yet single extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S205 T1 ;"));
}
THEN("unused extruder correctly ignored") {
REQUIRE(! Slic3r::Test::contains_regex(gcode, "M104 S\\d+ T0"));
}
}
WHEN("Printing with two extruders") {
config.opt_int("infill_extruder") = 1;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature set correctly for first extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S200 T0 ;"));
};
THEN("temperature set correctly for second extruder") {
REQUIRE(Slic3r::Test::contains(gcode, "\nM104 S205 T1 ;"));
};
}
auto test = [](DynamicPrintConfig &config) {
// 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
config.opt_int("infill_extruder") = 1;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("temperature placeholder for first extruder correctly populated") {
REQUIRE(Slic3r::Test::contains(gcode, "temp0:200"));
}
THEN("temperature placeholder for second extruder correctly populated") {
REQUIRE(Slic3r::Test::contains(gcode, "temp1:205"));
}
THEN("temperature placeholder for unused extruder populated with first value") {
REQUIRE(Slic3r::Test::contains(gcode, "temp2:200"));
}
};
WHEN("legacy syntax") {
config.set_deserialize_strict("start_gcode",
";__temp0:[first_layer_temperature_0]__\n"
";__temp1:[first_layer_temperature_1]__\n"
";__temp2:[first_layer_temperature_2]__\n");
test(config);
}
WHEN("new syntax") {
config.set_deserialize_strict("start_gcode",
";__temp0:{first_layer_temperature[0]}__\n"
";__temp1:{first_layer_temperature[1]}__\n"
";__temp2:{first_layer_temperature[2]}__\n");
test(config);
}
WHEN("Vojtech's syntax") {
config.set_deserialize_strict({
{ "infill_extruder", 1 },
{ "start_gcode",
";substitution:{if infill_extruder==1}extruder1"
"{elsif infill_extruder==2}extruder2"
"{else}extruder3{endif}"
}
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("if / else / endif - first block returned") {
REQUIRE(Test::contains(gcode, "\n;substitution:extruder1\n"));
}
}
GIVEN("Layer change G-codes")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "before_layer_gcode", ";BEFORE [layer_num]" },
{ "layer_gcode", ";CHANGE [layer_num]" },
{ "support_material", 1 },
{ "layer_height", 0.2 }
});
WHEN("before and after layer change G-codes set") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config);
GCodeReader parser;
std::vector<int> before;
std::vector<int> change;
parser.parse_buffer(gcode, [&before, &change](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line){
int d;
if (sscanf(line.raw().c_str(), ";BEFORE %d", &d) == 1)
before.emplace_back(d);
else if (sscanf(line.raw().c_str(), ";CHANGE %d", &d) == 1) {
change.emplace_back(d);
if (d != before.back())
throw std::runtime_error("inconsistent layer_num before and after layer change");
}
});
THEN("layer_num is consistent before and after layer changes") {
REQUIRE(before == change);
}
THEN("layer_num grows continously") {
// i.e. no duplicates or regressions
bool successive = true;
for (size_t i = 1; i < change.size(); ++ i)
if (change[i - 1] + 1 != change[i])
successive = false;
REQUIRE(successive);
}
}
}
GIVEN("if / elsif / elsif / elsif / else / endif")
{
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6,0.6 } },
{ "start_gcode",
";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}"
":end"
}
});
std::string returned[] = { "" /* indexed by one based extruder ID */, "if block", "elsif block 1", "elsif block 2", "elsif block 3", "endif block" };
auto test = [&config, &returned](int i) {
config.set_deserialize_strict({ { "infill_extruder", i } });
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
int found_error = 0;
for (int j = 1; j <= 5; ++ j)
if (i != j && Slic3r::Test::contains(gcode, std::string("substitution:") + returned[j] + ":end"))
// failure
++ found_error;
THEN(std::string("if / else / endif returned ") + returned[i]) {
REQUIRE(Slic3r::Test::contains(gcode, std::string("substitution:") + returned[i] + ":end"));
}
THEN(std::string("if / else / endif - only ") + returned[i] + "returned") {
REQUIRE(found_error == 0);
}
};
WHEN("infill_extruder == 1") { test(1); }
WHEN("infill_extruder == 2") { test(2); }
WHEN("infill_extruder == 3") { test(3); }
WHEN("infill_extruder == 4") { test(4); }
WHEN("infill_extruder == 5") { test(5); }
}
GIVEN("nested if / if / else / endif") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", { 0.6,0.6,0.6,0.6,0.6 } },
{ "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"
}
});
auto test = [&config](int i) {
config.opt_int("infill_extruder") = i;
int failed = 0;
for (int j = 1; j <= 2; ++ j) {
config.opt_int("perimeter_extruder") = j;
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
if (! Slic3r::Test::contains(gcode, std::string("substitution:block") + std::to_string(i) + std::to_string(j) + ":end"))
++ failed;
}
THEN(std::string("two level if / else / endif - block for infill_extruder ") + std::to_string(i) + "succeeded") {
REQUIRE(failed == 0);
}
};
WHEN("infill_extruder == 1") { test(1); }
WHEN("infill_extruder == 2") { test(2); }
WHEN("infill_extruder == 3") { test(3); }
}
GIVEN("printer type in notes") {
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "start_gcode",
";substitution:{if notes==\"MK2\"}MK2{elsif notes==\"MK3\"}MK3{else}MK1{endif}:end"
}
});
auto test = [&config](const std::string &printer_name) {
config.set_deserialize_strict("notes", printer_name);
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN(std::string("printer name ") + printer_name + " matched") {
REQUIRE(Slic3r::Test::contains(gcode, std::string("substitution:") + printer_name + ":end"));
}
};
WHEN("printer MK2") { test("MK2"); }
WHEN("printer MK3") { test("MK3"); }
WHEN("printer MK1") { test("MK1"); }
}
GIVEN("sequential print with between_objects_gcode") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "complete_objects", 1 },
{ "between_objects_gcode", "_MY_CUSTOM_GCODE_" }
});
std::string gcode = Slic3r::Test::slice(
// 3x 20mm box
{ Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20, Slic3r::Test::TestMesh::cube_20x20x20 },
config);
THEN("between_objects_gcode is applied correctly") {
const boost::regex expression("^_MY_CUSTOM_GCODE_");
const std::ptrdiff_t match_count =
std::distance(boost::sregex_iterator(gcode.begin(), gcode.end(), expression), boost::sregex_iterator());
REQUIRE(match_count == 2);
}
}
GIVEN("before_layer_gcode increments global variable") {
auto config = Slic3r::DynamicPrintConfig::new_with({
{ "start_gcode", "{global counter=0}" },
{ "before_layer_gcode", ";Counter{counter=counter+1;counter}\n" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("The counter is emitted multiple times before layer change.") {
REQUIRE(Slic3r::Test::contains(gcode, ";Counter1\n"));
REQUIRE(Slic3r::Test::contains(gcode, ";Counter2\n"));
REQUIRE(Slic3r::Test::contains(gcode, ";Counter3\n"));
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,90 @@
#ifndef SLIC3R_TEST_DATA_HPP
#define SLIC3R_TEST_DATA_HPP
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <unordered_map>
namespace Slic3r { namespace Test {
constexpr double MM_PER_MIN = 60.0;
/// Enumeration of test meshes
enum class TestMesh {
A,
L,
V,
_40x10,
cube_20x20x20,
cube_2x20x10,
sphere_50mm,
bridge,
bridge_with_hole,
cube_with_concave_hole,
cube_with_hole,
gt2_teeth,
ipadstand,
overhang,
pyramid,
sloping_hole,
slopy_cube,
small_dorito,
step,
two_hollow_squares
};
// Neccessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
};
/// Mesh enumeration to name mapping
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
/// Port of Slic3r::Test::mesh
/// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it
TriangleMesh mesh(TestMesh m);
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
/// Templated function to see if two values are equivalent (+/- epsilon)
template <typename T>
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string gcode(Print& print);
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
bool contains(const std::string &data, const std::string &pattern);
bool contains_regex(const std::string &data, const std::string &pattern);
} } // namespace Slic3r::Test
#endif // SLIC3R_TEST_DATA_HPP

View File

@@ -0,0 +1,409 @@
#include <catch2/catch.hpp>
#include <cstdlib>
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
static inline Slic3r::Point random_point(float LO=-50, float HI=50)
{
Vec2f pt = Vec2f(LO, LO) + (Vec2d(rand(), rand()) * (HI-LO) / RAND_MAX).cast<float>();
return pt.cast<coord_t>();
}
// build a sample extrusion entity collection with random start and end points.
static Slic3r::ExtrusionPath random_path(size_t length = 20, float LO = -50, float HI = 50)
{
ExtrusionPath t { ExtrusionRole::Perimeter, 1.0, 1.0, 1.0 };
for (size_t j = 0; j < length; ++ j)
t.polyline.append(random_point(LO, HI));
return t;
}
static Slic3r::ExtrusionPaths random_paths(size_t count = 10, size_t length = 20, float LO = -50, float HI = 50)
{
Slic3r::ExtrusionPaths p;
for (size_t i = 0; i < count; ++ i)
p.push_back(random_path(length, LO, HI));
return p;
}
SCENARIO("ExtrusionPath", "[ExtrusionEntity]") {
GIVEN("Simple path") {
Slic3r::ExtrusionPath path{ ExtrusionRole::ExternalPerimeter };
path.polyline = { { 100, 100 }, { 200, 100 }, { 200, 200 } };
path.mm3_per_mm = 1.;
THEN("first point") {
REQUIRE(path.first_point() == path.polyline.front());
}
THEN("cloned") {
auto cloned = std::unique_ptr<ExtrusionEntity>(path.clone());
REQUIRE(cloned->role() == path.role());
}
}
}
static ExtrusionPath new_extrusion_path(const Polyline &polyline, ExtrusionRole role, double mm3_per_mm)
{
ExtrusionPath path(role);
path.polyline = polyline;
path.mm3_per_mm = 1.;
return path;
}
SCENARIO("ExtrusionLoop", "[ExtrusionEntity]")
{
GIVEN("Square") {
Polygon square { { 100, 100 }, { 200, 100 }, { 200, 200 }, { 100, 200 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(square.split_at_first_point(), ExtrusionRole::ExternalPerimeter, 1.));
THEN("polygon area") {
REQUIRE(loop.polygon().area() == Approx(square.area()));
}
THEN("loop length") {
REQUIRE(loop.length() == Approx(square.length()));
}
WHEN("cloned") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
THEN("cloning worked") {
REQUIRE(loop2 != nullptr);
}
THEN("loop contains one path") {
REQUIRE(loop2->paths.size() == 1);
}
THEN("cloned role") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::ExternalPerimeter);
}
}
WHEN("cloned and split") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(square.points[2]);
THEN("splitting a single-path loop results in a single path") {
REQUIRE(loop2->paths.size() == 1);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().size() == 5);
}
THEN("expected point order") {
REQUIRE(loop2->paths.front().polyline[0] == square.points[2]);
REQUIRE(loop2->paths.front().polyline[1] == square.points[3]);
REQUIRE(loop2->paths.front().polyline[2] == square.points[0]);
REQUIRE(loop2->paths.front().polyline[3] == square.points[1]);
REQUIRE(loop2->paths.front().polyline[4] == square.points[2]);
}
}
}
GIVEN("Loop with two pieces") {
Polyline polyline1 { { 100, 100 }, { 200, 100 }, { 200, 200 } };
Polyline polyline2 { { 200, 200 }, { 100, 200 }, { 100, 100 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(polyline1, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline2, ExtrusionRole::OverhangPerimeter, 1.));
double tot_len = polyline1.length() + polyline2.length();
THEN("length") {
REQUIRE(loop.length() == Approx(tot_len));
}
WHEN("splitting at intermediate point") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(polyline1.points[1]);
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
THEN("loop contains three paths after splitting") {
REQUIRE(loop2->paths.size() == 3);
}
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == polyline1.points[1]);
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == polyline1.points[1]);
}
THEN("paths have common point") {
REQUIRE(loop2->paths.front().polyline.back() == loop2->paths[1].polyline.front());
REQUIRE(loop2->paths[1].polyline.back() == loop2->paths[2].polyline.front());
}
THEN("expected order after splitting") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop2->paths[1].role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop2->paths[2].role() == ExtrusionRole::ExternalPerimeter);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().polyline.size() == 2);
REQUIRE(loop2->paths[1].polyline.size() == 3);
REQUIRE(loop2->paths[2].polyline.size() == 2);
}
THEN("clipped path has expected length") {
double l = loop2->length();
ExtrusionPaths paths;
loop2->clip_end(3, &paths);
double l2 = 0;
for (const ExtrusionPath &p : paths)
l2 += p.length();
REQUIRE(l2 == Approx(l - 3.));
}
}
WHEN("splitting at endpoint") {
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at_vertex(polyline2.points.front());
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
THEN("loop contains two paths after splitting") {
REQUIRE(loop2->paths.size() == 2);
}
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == polyline2.points.front());
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == polyline2.points.front());
}
THEN("paths have common point") {
REQUIRE(loop2->paths.front().polyline.back() == loop2->paths[1].polyline.front());
REQUIRE(loop2->paths[1].polyline.back() == loop2->paths.front().polyline.front());
}
THEN("expected order after splitting") {
REQUIRE(loop2->paths.front().role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop2->paths[1].role() == ExtrusionRole::ExternalPerimeter);
}
THEN("path has correct number of points") {
REQUIRE(loop2->paths.front().polyline.size() == 3);
REQUIRE(loop2->paths[1].polyline.size() == 3);
}
}
WHEN("splitting at an edge") {
Point point(250, 150);
auto loop2 = std::unique_ptr<ExtrusionLoop>(dynamic_cast<ExtrusionLoop*>(loop.clone()));
loop2->split_at(point, false, 0);
THEN("length after splitting is unchanged") {
REQUIRE(loop2->length() == Approx(tot_len));
}
Point expected_start_point(200, 150);
THEN("expected starting point") {
REQUIRE(loop2->paths.front().polyline.front() == expected_start_point);
}
THEN("expected ending point") {
REQUIRE(loop2->paths.back().polyline.back() == expected_start_point);
}
}
}
GIVEN("Loop with four pieces") {
Polyline polyline1 { { 59312736, 4821067 }, { 64321068, 4821067 }, { 64321068, 4821067 }, { 64321068, 9321068 }, { 59312736, 9321068 } };
Polyline polyline2 { { 59312736, 9321068 }, { 9829401, 9321068 } };
Polyline polyline3 { { 9829401, 9321068 }, { 4821067, 9321068 }, { 4821067, 4821067 }, { 9829401, 4821067 } };
Polyline polyline4 { { 9829401, 4821067 }, { 59312736,4821067 } };
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(polyline1, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline2, ExtrusionRole::OverhangPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline3, ExtrusionRole::ExternalPerimeter, 1.));
loop.paths.emplace_back(new_extrusion_path(polyline4, ExtrusionRole::OverhangPerimeter, 1.));
double len = loop.length();
WHEN("splitting at vertex") {
Point point(4821067, 9321068);
if (! loop.split_at_vertex(point))
loop.split_at(point, false, 0);
THEN("total length is preserved after splitting") {
REQUIRE(loop.length() == Approx(len));
}
THEN("order is correctly preserved after splitting") {
REQUIRE(loop.paths.front().role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop.paths[1].role() == ExtrusionRole::OverhangPerimeter);
REQUIRE(loop.paths[2].role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(loop.paths[3].role() == ExtrusionRole::OverhangPerimeter);
}
}
}
GIVEN("Some complex loop") {
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(
Polyline { { 15896783, 15868739 }, { 24842049, 12117558 }, { 33853238, 15801279 }, { 37591780, 24780128 }, { 37591780, 24844970 },
{ 33853231, 33825297 }, { 24842049, 37509013 }, { 15896798, 33757841 }, { 12211841, 24812544 }, { 15896783, 15868739 } },
ExtrusionRole::ExternalPerimeter, 1.));
double len = loop.length();
THEN("split_at() preserves total length") {
loop.split_at({ 15896783, 15868739 }, false, 0);
REQUIRE(loop.length() == Approx(len));
}
}
}
SCENARIO("ExtrusionEntityCollection: Basics", "[ExtrusionEntity]")
{
Polyline polyline { { 100, 100 }, { 200, 100 }, { 200, 200 } };
ExtrusionPath path = new_extrusion_path(polyline, ExtrusionRole::ExternalPerimeter, 1.);
ExtrusionLoop loop;
loop.paths.emplace_back(new_extrusion_path(Polygon(polyline.points).split_at_first_point(), ExtrusionRole::InternalInfill, 1.));
ExtrusionEntityCollection collection;
collection.append(path);
THEN("no_sort is false by default") {
REQUIRE(! collection.no_sort);
}
collection.append(collection);
THEN("append ExtrusionEntityCollection") {
REQUIRE(collection.entities.size() == 2);
}
collection.append(path);
THEN("append ExtrusionPath") {
REQUIRE(collection.entities.size() == 3);
}
collection.append(loop);
THEN("append ExtrusionLoop") {
REQUIRE(collection.entities.size() == 4);
}
THEN("appended collection was duplicated") {
REQUIRE(dynamic_cast<ExtrusionEntityCollection*>(collection.entities[1])->entities.size() == 1);
}
WHEN("cloned") {
auto coll2 = std::unique_ptr<ExtrusionEntityCollection>(dynamic_cast<ExtrusionEntityCollection*>(collection.clone()));
THEN("expected no_sort value") {
assert(! coll2->no_sort);
}
coll2->no_sort = true;
THEN("no_sort is kept after clone") {
auto coll3 = std::unique_ptr<ExtrusionEntityCollection>(dynamic_cast<ExtrusionEntityCollection*>(coll2->clone()));
assert(coll3->no_sort);
}
}
}
SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]")
{
srand(0xDEADBEEF); // consistent seed for test reproducibility.
// Generate one specific random path set and save it for later comparison
Slic3r::ExtrusionPaths nosort_path_set = random_paths();
Slic3r::ExtrusionEntityCollection sub_nosort;
sub_nosort.append(nosort_path_set);
sub_nosort.no_sort = true;
Slic3r::ExtrusionEntityCollection sub_sort;
sub_sort.no_sort = false;
sub_sort.append(random_paths());
GIVEN("A Extrusion Entity Collection with a child that has one child that is marked as no-sort") {
Slic3r::ExtrusionEntityCollection sample;
Slic3r::ExtrusionEntityCollection output;
sample.append(sub_sort);
sample.append(sub_nosort);
sample.append(sub_sort);
WHEN("The EEC is flattened with default options (preserve_order=false)") {
output = sample.flatten();
THEN("The output EEC contains no Extrusion Entity Collections") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 0);
}
}
WHEN("The EEC is flattened with preservation (preserve_order=true)") {
output = sample.flatten(true);
THEN("The output EECs contains one EEC.") {
CHECK(std::count_if(output.entities.cbegin(), output.entities.cend(), [=](const ExtrusionEntity* e) {return e->is_collection();}) == 1);
}
AND_THEN("The ordered EEC contains the same order of elements than the original") {
// find the entity in the collection
for (auto e : output.entities)
if (e->is_collection()) {
ExtrusionEntityCollection *temp = dynamic_cast<ExtrusionEntityCollection*>(e);
// check each Extrusion path against nosort_path_set to see if the first and last match the same
CHECK(nosort_path_set.size() == temp->entities.size());
for (size_t i = 0; i < nosort_path_set.size(); ++ i) {
CHECK(temp->entities[i]->first_point() == nosort_path_set[i].first_point());
CHECK(temp->entities[i]->last_point() == nosort_path_set[i].last_point());
}
}
}
}
}
}
TEST_CASE("ExtrusionEntityCollection: Chained path", "[ExtrusionEntity]") {
struct Test {
Polylines unchained;
Polylines chained;
Point initial_point;
};
std::vector<Test> tests {
{
{
{ {0,15}, {0,18}, {0,20} },
{ {0,10}, {0,8}, {0,5} }
},
{
{ {0,20}, {0,18}, {0,15} },
{ {0,10}, {0,8}, {0,5} }
},
{ 0, 30 }
},
{
{
{ {4,0}, {10,0}, {15,0} },
{ {10,5}, {15,5}, {20,5} }
},
{
{ {20,5}, {15,5}, {10,5} },
{ {15,0}, {10,0}, {4,0} }
},
{ 30, 0 }
},
{
{
{ {15,0}, {10,0}, {4,0} },
{ {10,5}, {15,5}, {20,5} }
},
{
{ {20,5}, {15,5}, {10,5} },
{ {15,0}, {10,0}, {4,0} }
},
{ 30, 0 }
},
};
for (const Test &test : tests) {
Polylines chained = chain_polylines(test.unchained, &test.initial_point);
REQUIRE(chained == test.chained);
ExtrusionEntityCollection unchained_extrusions;
extrusion_entities_append_paths(unchained_extrusions.entities, test.unchained,
ExtrusionRole::InternalInfill, 0., 0.4f, 0.3f);
THEN("Chaining works") {
ExtrusionEntityCollection chained_extrusions = unchained_extrusions.chained_path_from(test.initial_point);
REQUIRE(chained_extrusions.entities.size() == test.chained.size());
for (size_t i = 0; i < chained_extrusions.entities.size(); ++ i) {
const Points &p1 = test.chained[i].points;
const Points &p2 = dynamic_cast<const ExtrusionPath*>(chained_extrusions.entities[i])->polyline.points;
REQUIRE(p1 == p2);
}
}
THEN("Chaining produces no change with no_sort") {
unchained_extrusions.no_sort = true;
ExtrusionEntityCollection chained_extrusions = unchained_extrusions.chained_path_from(test.initial_point);
REQUIRE(chained_extrusions.entities.size() == test.unchained.size());
for (size_t i = 0; i < chained_extrusions.entities.size(); ++ i) {
const Points &p1 = test.unchained[i].points;
const Points &p2 = dynamic_cast<const ExtrusionPath*>(chained_extrusions.entities[i])->polyline.points;
REQUIRE(p1 == p2);
}
}
}
}
TEST_CASE("ExtrusionEntityCollection: Chained path with no explicit starting point", "[ExtrusionEntity]") {
auto polylines = Polylines { { { 0, 15 }, {0, 18}, {0, 20} }, { { 0, 10 }, {0, 8}, {0, 5} } };
auto target = Polylines { { {0, 5}, {0, 8}, { 0, 10 } }, { { 0, 15 }, {0, 18}, {0, 20} } };
auto chained = chain_polylines(polylines);
REQUIRE(chained == target);
}

View File

@@ -0,0 +1,715 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/libslic3r.h"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/SVG.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std::literals;
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
#if 0
TEST_CASE("Fill: adjusted solid distance") {
int surface_width = 250;
int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
REQUIRE(distance == Approx(50));
REQUIRE(surface_width % distance == 0);
}
#endif
TEST_CASE("Fill: 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 { {0,0}, {100,0}, {100,100}, {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 produces one continuous path") {
Slic3r::ExPolygon expolygon(Polygon::new_scale({ {0, 0}, {50, 0}, {50, 50}, {0, 50} }));
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
filler->bounding_box = get_extents(expolygon);
filler->angle = 0;
Surface surface(stTop, expolygon);
// width, height, nozzle_dmr
auto flow = Slic3r::Flow(0.69f, 0.4f, 0.5f);
FillParams fill_params;
for (auto density : { 0.4, 1.0 }) {
fill_params.density = density;
filler->spacing = flow.spacing();
REQUIRE(!fill_params.use_arachne); // Make this test fail when Arachne is used because this test is not ready for it.
for (auto angle : { 0.0, 45.0}) {
surface.expolygon.rotate(angle, Point(0,0));
Polylines paths = filler->fill_surface(&surface, fill_params);
// one continuous path
REQUIRE(paths.size() == 1);
}
}
}
#if 0 // Disabled temporarily due to precission 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);
}
}
SCENARIO("Infill does not exceed perimeters", "[Fill]")
{
auto test = [](const std::string_view pattern) {
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 },
{ "bottom_fill_pattern", pattern },
{ "perimeters", 1 },
{ "skirts", 0 },
{ "fill_density", 0.2 },
{ "layer_height", 0.05 },
{ "perimeter_extruder", 1 },
{ "infill_extruder", 2 }
});
WHEN("40mm cube sliced") {
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::cube_20x20x20, Vec3d::Zero(), 2.0) }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("infill does not exceed perimeters") {
GCodeReader parser;
const int perimeter_extruder = config.opt_int("perimeter_extruder");
const int infill_extruder = config.opt_int("infill_extruder");
int tool = -1;
Points perimeter_points;
Points infill_points;
parser.parse_buffer(gcode, [&tool, &perimeter_points, &infill_points, perimeter_extruder, infill_extruder]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1) + 1;
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == perimeter_extruder)
perimeter_points.emplace_back(line.new_XY_scaled(self));
else if (tool == infill_extruder)
infill_points.emplace_back(line.new_XY_scaled(self));
}
});
auto convex_hull = Geometry::convex_hull(perimeter_points);
int num_inside = std::count_if(infill_points.begin(), infill_points.end(), [&convex_hull](const Point &pt){ return convex_hull.contains(pt); });
REQUIRE(num_inside == infill_points.size());
}
}
};
GIVEN("Rectilinear") { test("rectilinear"sv); }
GIVEN("Honeycomb") { test("honeycomb"sv); }
GIVEN("HilbertCurve") { test("hilbertcurve"sv); }
GIVEN("Concentric") { test("concentric"sv); }
}
// SCENARIO("Infill only where needed", "[Fill]")
// {
// DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
// config.set_deserialize_strict({
// { "nozzle_diameter", "0.4, 0.4, 0.4, 0.4" },
// { "infill_only_where_needed", true },
// { "bottom_solid_layers", 0 },
// { "infill_extruder", 2 },
// { "infill_extrusion_width", 0.5 },
// { "wipe_into_infill", false },
// { "fill_density", 0.4 },
// // for preventing speeds from being altered
// { "cooling", "0, 0, 0, 0" },
// // for preventing speeds from being altered
// { "first_layer_speed", "100%" }
// });
// auto test = [&config]() -> double {
// TriangleMesh pyramid = Test::mesh(Slic3r::Test::TestMesh::pyramid);
// // Arachne doesn't use "Detect thin walls," and because of this, it filters out tiny infill areas differently.
// // So, for Arachne, we cut the pyramid model to achieve similar results.
// if (config.opt_enum<PerimeterGeneratorType>("perimeter_generator") == Slic3r::PerimeterGeneratorType::Arachne) {
// indexed_triangle_set lower{};
// cut_mesh(pyramid.its, 35, nullptr, &lower);
// pyramid = TriangleMesh(lower);
// }
// std::string gcode = Slic3r::Test::slice({ pyramid }, config);
// THEN("gcode not empty") {
// REQUIRE(! gcode.empty());
// }
// GCodeReader parser;
// int tool = -1;
// const int infill_extruder = config.opt_int("infill_extruder");
// Points infill_points;
// parser.parse_buffer(gcode, [&tool, &infill_points, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
// {
// // if the command is a T command, set the the current tool
// if (boost::starts_with(line.cmd(), "T")) {
// tool = atoi(line.cmd().data() + 1) + 1;
// } else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
// if (tool == infill_extruder) {
// infill_points.emplace_back(self.xy_scaled());
// infill_points.emplace_back(line.new_XY_scaled(self));
// }
// }
// });
// // prevent calling convex_hull() with no points
// THEN("infill not empty") {
// REQUIRE(! infill_points.empty());
// }
// auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
// REQUIRE(! opt_width->percent);
// Polygons convex_hull = expand(Geometry::convex_hull(infill_points), scaled<float>(opt_width->value / 2));
// return SCALING_FACTOR * SCALING_FACTOR * std::accumulate(convex_hull.begin(), convex_hull.end(), 0., [](double acc, const Polygon &poly){ return acc + poly.area(); });
// };
// double tolerance = 5; // mm^2
// // GIVEN("solid_infill_below_area == 0") {
// // config.opt_float("solid_infill_below_area") = 0;
// // WHEN("pyramid is sliced ") {
// // auto area = test();
// // THEN("no infill is generated when using infill_only_where_needed on a pyramid") {
// // REQUIRE(area < tolerance);
// // }
// // }
// // }
// // GIVEN("solid_infill_below_area == 70") {
// // config.opt_float("solid_infill_below_area") = 70;
// // WHEN("pyramid is sliced ") {
// // auto area = test();
// // THEN("infill is only generated under the forced solid shells") {
// // REQUIRE(std::abs(area - 70) < tolerance);
// // }
// // }
// // }
// }
SCENARIO("Combine infill", "[Fill]")
{
{
auto test = [](const DynamicPrintConfig &config) {
std::string gcode = Test::slice({ Test::TestMesh::cube_20x20x20 }, config);
THEN("infill_every_layers does not crash") {
REQUIRE(! gcode.empty());
}
Slic3r::GCodeReader parser;
int tool = -1;
std::set<coord_t> layers; // layer_z => 1
std::map<coord_t, bool> layer_infill; // layer_z => has_infill
const int infill_extruder = config.opt_int("infill_extruder");
const int support_material_extruder = config.opt_int("support_material_extruder");
parser.parse_buffer(gcode,
[&tool, &layers, &layer_infill, infill_extruder, support_material_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
coord_t z = line.new_Z(self) / SCALING_FACTOR;
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0 && tool + 1 != support_material_extruder) {
if (tool + 1 == infill_extruder)
layer_infill[z] = true;
else if (auto it = layer_infill.find(z); it == layer_infill.end())
layer_infill.insert(it, std::make_pair(z, false));
}
// Previously, all G-code commands had a fixed number of decimal points with means with redundant zeros after decimal points.
// We changed this behavior and got rid of these redundant padding zeros, which caused this test to fail
// because the position in Z-axis is compared as a string, and previously, G-code contained the following two commands:
// "G1 Z5 F5000 ; lift nozzle"
// "G1 Z5.000 F7800.000"
// That has a different Z-axis position from the view of string comparisons of floating-point numbers.
// To correct the computation of the number of printed layers, even in the case of string comparisons of floating-point numbers,
// we filtered out the G-code command with the commend 'lift nozzle'.
if (line.cmd_is("G1") && line.dist_Z(self) != 0 && line.comment().find("lift nozzle") == std::string::npos)
layers.insert(z);
});
auto layers_with_perimeters = int(layer_infill.size());
auto layers_with_infill = int(std::count_if(layer_infill.begin(), layer_infill.end(), [](auto &v){ return v.second; }));
THEN("expected number of layers") {
REQUIRE(layers.size() == layers_with_perimeters + config.opt_int("raft_layers"));
}
if (config.opt_int("raft_layers") == 0) {
// first infill layer printed directly on print bed is not combined, so we don't consider it.
-- layers_with_infill;
-- layers_with_perimeters;
}
// we expect that infill is generated for half the number of combined layers
// plus for each single layer that was not combined (remainder)
THEN("infill is only present in correct number of layers") {
int infill_every = config.opt_int("infill_every_layers");
REQUIRE(layers_with_infill == int(layers_with_perimeters / infill_every) + (layers_with_perimeters % infill_every));
}
};
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.5, 0.5, 0.5, 0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 2 },
{ "perimeter_extruder", 1 },
{ "infill_extruder", 2 },
{ "wipe_into_infill", false },
{ "support_material_extruder", 3 },
{ "support_material_interface_extruder", 3 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 }
});
test(config);
// Reuse the config above
config.set_deserialize_strict({
{ "skirts", 0 }, // prevent usage of perimeter_extruder in raft layers
{ "raft_layers", 5 }
});
test(config);
}
WHEN("infill_every_layers == 2") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ Test::TestMesh::cube_20x20x20 }, print, {
{ "nozzle_diameter", "0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 2 }
});
THEN("infill combination produces internal void surfaces") {
bool has_void = false;
for (const Layer *layer : print.get_object(0)->layers())
if (layer->get_region(0)->fill_surfaces().filter_by_type(stInternalVoid).size() > 0) {
has_void = true;
break;
}
REQUIRE(has_void);
}
}
WHEN("infill_every_layers disabled") {
// we disable combination after infill has been generated
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ Test::TestMesh::cube_20x20x20 }, print, {
{ "nozzle_diameter", "0.5" },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "infill_every_layers", 1 }
});
THEN("infill combination is idempotent") {
bool has_infill_on_each_layer = true;
for (const Layer *layer : print.get_object(0)->layers())
if (layer->get_region(0)->fill_surfaces().empty()) {
has_infill_on_each_layer = false;
break;
}
REQUIRE(has_infill_on_each_layer);
}
}
}
SCENARIO("Infill density zero", "[Fill]")
{
WHEN("20mm cube is sliced") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 0 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "solid_infill_below_area", 20000000 },
{ "solid_infill_every_layers", 2 },
{ "perimeter_speed", 99 },
{ "external_perimeter_speed", 99 },
{ "cooling", "0" },
{ "first_layer_speed", "100%" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("solid_infill_below_area and solid_infill_every_layers are ignored when fill_density is 0") {
GCodeReader parser;
const double perimeter_speed = config.opt_float("perimeter_speed");
std::map<double, double> layers_with_extrusion;
parser.parse_buffer(gcode, [&layers_with_extrusion, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
if (std::abs(f - perimeter_speed * 60.) > 0.01)
// It is a perimeter.
layers_with_extrusion[self.z()] = f;
}
});
REQUIRE(layers_with_extrusion.empty());
}
}
WHEN("A is sliced") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "fill_density", 0 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "nozzle_diameter", "0.35,0.35,0.35,0.35" },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 2 },
{ "infill_extrusion_width", 0.52 },
{ "solid_infill_extrusion_width", 0.52 },
{ "first_layer_extrusion_width", 0 }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::A }, config);
THEN("gcode not empty") {
REQUIRE(! gcode.empty());
}
THEN("no missing parts in solid shell when fill_density is 0") {
GCodeReader parser;
int tool = -1;
const int infill_extruder = config.opt_int("infill_extruder");
std::map<coord_t, Lines> infill;
parser.parse_buffer(gcode, [&tool, &infill, infill_extruder](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1) + 1;
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == infill_extruder)
infill[scaled<coord_t>(self.z())].emplace_back(self.xy_scaled(), line.new_XY_scaled(self));
}
});
auto opt_width = config.opt<ConfigOptionFloatOrPercent>("infill_extrusion_width");
REQUIRE(! opt_width->percent);
auto grow_d = scaled<float>(opt_width->value / 2);
auto inflate_lines = [grow_d](const Lines &lines) {
Polygons out;
for (const Line &line : lines)
append(out, offset(Polyline{ line.a, line.b }, grow_d, Slic3r::ClipperLib::jtSquare, 3.));
return union_(out);
};
Polygons layer0_infill = inflate_lines(infill[scaled<coord_t>(0.2)]);
Polygons layer1_infill = inflate_lines(infill[scaled<coord_t>(0.4)]);
ExPolygons poly = opening_ex(diff_ex(layer0_infill, layer1_infill), grow_d);
const double threshold = 2. * sqr(grow_d * 2.);
int missing_parts = std::count_if(poly.begin(), poly.end(), [threshold](const ExPolygon &poly){ return poly.area() > threshold; });
REQUIRE(missing_parts == 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';
}
{
skip "The FillRectilinear2 does not fill the surface completely", 1;
my $test = sub {
my ($expolygon, $flow_spacing, $angle, $density) = @_;
my $filler = Slic3r::Filler->new_from_type('rectilinear');
$filler->set_bounding_box($expolygon->bounding_box);
$filler->set_angle($angle // 0);
# Adjust line spacing to fill the region.
$filler->set_dont_adjust(0);
$filler->set_link_max_length(scale(1.2*$flow_spacing));
my $surface = Slic3r::Surface->new(
surface_type => S_TYPE_BOTTOM,
expolygon => $expolygon,
);
my $flow = Slic3r::Flow->new(
width => $flow_spacing,
height => 0.4,
nozzle_diameter => $flow_spacing,
);
$filler->set_spacing($flow->spacing);
my $paths = $filler->fill_surface(
$surface,
layer_height => $flow->height,
density => $density // 1,
);
# check whether any part was left uncovered
my @grown_paths = map @{Slic3r::Polyline->new(@$_)->grow(scale $filler->spacing/2)}, @$paths;
my $uncovered = diff_ex([ @$expolygon ], [ @grown_paths ], 1);
# ignore very small dots
my $uncovered_filtered = [ grep $_->area > (scale $flow_spacing)**2, @$uncovered ];
is scalar(@$uncovered_filtered), 0, 'solid surface is fully filled';
if (0 && @$uncovered_filtered) {
require "Slic3r/SVG.pm";
Slic3r::SVG::output("uncovered.svg",
no_arrows => 1,
expolygons => [ $expolygon ],
blue_expolygons => [ @$uncovered ],
red_expolygons => [ @$uncovered_filtered ],
polylines => [ @$paths ],
);
exit;
}
};
my $expolygon = Slic3r::ExPolygon->new([
[6883102, 9598327.01296997],
[6883102, 20327272.01297],
[3116896, 20327272.01297],
[3116896, 9598327.01296997],
]);
$test->($expolygon, 0.55);
for (1..20) {
$expolygon->scale(1.05);
$test->($expolygon, 0.55);
}
$expolygon = Slic3r::ExPolygon->new(
[[59515297,5422499],[59531249,5578697],[59695801,6123186],[59965713,6630228],[60328214,7070685],[60773285,7434379],[61274561,7702115],[61819378,7866770],[62390306,7924789],[62958700,7866744],[63503012,7702244],[64007365,7434357],[64449960,7070398],[64809327,6634999],[65082143,6123325],[65245005,5584454],[65266967,5422499],[66267307,5422499],[66269190,8310081],[66275379,17810072],[66277259,20697500],[65267237,20697500],[65245004,20533538],[65082082,19994444],[64811462,19488579],[64450624,19048208],[64012101,18686514],[63503122,18415781],[62959151,18251378],[62453416,18198442],[62390147,18197355],[62200087,18200576],[61813519,18252990],[61274433,18415918],[60768598,18686517],[60327567,19047892],[59963609,19493297],[59695865,19994587],[59531222,20539379],[59515153,20697500],[58502480,20697500],[58502480,5422499]]
);
$test->($expolygon, 0.524341649025257);
$expolygon = Slic3r::ExPolygon->new([ scale_points [0,0], [98,0], [98,10], [0,10] ]);
$test->($expolygon, 0.5, 45, 0.99); # non-solid infill
}
*/
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);
if (fill_params.use_arachne) // Make this test fail when Arachne is used because this test is not ready for it.
return false;
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
}

View File

@@ -0,0 +1,226 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "test_data.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Extrusion width specifics", "[Flow]") {
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);
}
}
}
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 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 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 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));
}
}
}
GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio, fixed extrusion width of 0.4mm and an overhang mesh.") {
WHEN("bridge_flow_ratio is set to 1.0") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 0.5") {
THEN("Output flow is as expected.") {
}
}
WHEN("bridge_flow_ratio is set to 2.0") {
THEN("Output flow is as expected.") {
}
}
}
}
/// Test the expected behavior for auto-width,
/// spacing, etc
SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
ConfigOptionFloatOrPercent width(1.0, false);
float nozzle_diameter = 0.4f;
float layer_height = 0.4f;
// Spacing for non-bridges is has some overlap
THEN("External perimeter flow has spacing fixed to 1.125 * nozzle_diameter") {
auto flow = Flow::new_from_config_width(frExternalPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(1.125 * nozzle_diameter - layer_height * (1.0 - PI / 4.0)));
}
THEN("Internal perimeter flow has spacing fixed to 1.125 * nozzle_diameter") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(1.125 *nozzle_diameter - layer_height * (1.0 - PI / 4.0)));
}
THEN("Spacing for supplied width is 0.8927f") {
auto flow = Flow::new_from_config_width(frExternalPerimeter, width, nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(width.value - layer_height * (1.0 - PI / 4.0)));
flow = Flow::new_from_config_width(frPerimeter, width, nozzle_diameter, layer_height);
REQUIRE(flow.spacing() == Approx(width.value - layer_height * (1.0 - PI / 4.0)));
}
}
/// Check the min/max
GIVEN("Nozzle Diameter of 0.25") {
float nozzle_diameter = 0.25f;
float layer_height = 0.5f;
WHEN("layer height is set to 0.2") {
layer_height = 0.15f;
THEN("Max width is set.") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.width() == Approx(1.125 * nozzle_diameter));
}
}
WHEN("Layer height is set to 0.25") {
layer_height = 0.25f;
THEN("Min width is set.") {
auto flow = Flow::new_from_config_width(frPerimeter, ConfigOptionFloatOrPercent(0, false), nozzle_diameter, layer_height);
REQUIRE(flow.width() == Approx(1.125 * nozzle_diameter));
}
}
}
#if 0
/// Check for an edge case in the maths where the spacing could be 0; original
/// math is 0.99. Slic3r issue #4654
GIVEN("Input spacing of 0.414159 and a total width of 2") {
double in_spacing = 0.414159;
double total_width = 2.0;
auto flow = Flow::new_from_spacing(1.0, 0.4, 0.3);
WHEN("solid_spacing() is called") {
double result = flow.solid_spacing(total_width, in_spacing);
THEN("Yielded spacing is greater than 0") {
REQUIRE(result > 0);
}
}
}
#endif
}
/// Spacing, width calculation for bridge extrusions
SCENARIO("Flow: Flow math for bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
float nozzle_diameter = 0.4f;
float bridge_flow = 1.0f;
WHEN("Flow role is frExternalPerimeter") {
auto flow = Flow::bridging_flow(nozzle_diameter * sqrt(bridge_flow), nozzle_diameter);
THEN("Bridge width is same as nozzle diameter") {
REQUIRE(flow.width() == Approx(nozzle_diameter));
}
THEN("Bridge spacing is same as nozzle diameter + BRIDGE_EXTRA_SPACING") {
REQUIRE(flow.spacing() == Approx(nozzle_diameter + BRIDGE_EXTRA_SPACING));
}
}
}
}

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,22 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCode.hpp"
using namespace Slic3r;
SCENARIO("Origin manipulation", "[GCode]") {
Slic3r::GCode gcodegen;
WHEN("set_origin to (10,0)") {
gcodegen.set_origin(Vec2d(10,0));
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
}
WHEN("set_origin to (10,0) and translate by (5, 5)") {
gcodegen.set_origin(Vec2d(10,0));
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
THEN("origin returns reference to point") {
REQUIRE(gcodegen.origin() == Vec2d(15,5));
}
}
}

View File

@@ -0,0 +1,292 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCode/FindReplace.hpp"
using namespace Slic3r;
SCENARIO("Find/Replace with plain text", "[GCodeFindReplace]") {
GIVEN("G-code") {
const std::string gcode =
"G1 Z0; home\n"
"G1 Z1; move up\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n";
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "i", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "i", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Whole word
WHEN("Replace \"move up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move up", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move u\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
WHEN("Replace \"ove up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Multi-line replace
WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word.
WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "w", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word, fails.
WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "w", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
GIVEN("G-code with decimals") {
const std::string gcode =
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y.33 Z.431 E1.2; perimeter\n";
WHEN("Regular expression NOT processed in non-regex mode") {
GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
}
SCENARIO("Find/Replace with regexp", "[GCodeFindReplace]") {
GIVEN("G-code") {
const std::string gcode =
"G1 Z0; home\n"
"G1 Z1; move up\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n";
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move up", "move down", "ri", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move UP\" with \"move down\", case insensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "ri", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move up\" with \"move down\", case sensitive") {
GCodeFindReplace find_replace({ "move UP", "move down", "r", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Whole word
WHEN("Replace \"move up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move up", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G1 X0 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
WHEN("Replace \"move u\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
WHEN("Replace \"ove up\" with \"move down\", whole word") {
GCodeFindReplace find_replace({ "move u", "move down", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
// Multi-line replace
WHEN("Replace \"move up\\nG1 X0 \" with \"move down\\nG0 X1 \"") {
GCodeFindReplace find_replace({ "move up\\nG1 X0 ", "move down\\nG0 X1 ", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word.
WHEN("Replace \"move up\\nG1 X0\" with \"move down\\nG0 X1\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X0", "move down\\nG0 X1", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0; home\n"
// substituted
"G1 Z1; move down\n"
"G0 X1 Y1 Z1; perimeter\n"
"G1 X13 Y32 Z1; infill\n"
"G1 X13 Y32 Z1; wipe\n");
}
// Multi-line replace, whole word, fails.
WHEN("Replace \"move up\\nG1 X\" with \"move down\\nG0 X\", whole word") {
GCodeFindReplace find_replace({ "move up\\nG1 X", "move down\\nG0 X", "rw", "" });
REQUIRE(find_replace.process_layer(gcode) == gcode);
}
}
GIVEN("G-code with decimals") {
const std::string gcode =
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y.33 Z.431 E1.2; perimeter\n";
WHEN("Missing zeros before dot filled in") {
GCodeFindReplace find_replace({ "( [XYZEF]-?)\\.([0-9]+)", "\\10.\\2", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z0.123; home\n"
"G1 Z1.21; move up\n"
"G1 X0 Y0.33 Z0.431 E1.2; perimeter\n");
}
}
GIVEN("Single layer G-code block with extrusion types") {
const std::string gcode =
// Start of a layer.
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n";
WHEN("Change extrusion rate of top solid infill, single line modifier") {
GCodeFindReplace find_replace({ "(;TYPE:Top solid infill\\n)(.*?)(;TYPE:[^T][^o][^p][^ ][^s]|$)", "${1}M221 S98\\n${2}M221 S95\\n${3}", "rs", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n");
}
WHEN("Change extrusion rate of top solid infill, no single line modifier (incorrect)") {
GCodeFindReplace find_replace({ "(;TYPE:Top solid infill\\n)(.*?)(;TYPE:[^T][^o][^p][^ ][^s]|$)", "${1}M221 S98\\n${2}\\nM221 S95${3}", "r", "" });
REQUIRE(find_replace.process_layer(gcode) ==
"G1 Z1.21; move up\n"
";TYPE:Infill\n"
"G1 X0 Y.33 Z.431 E1.2\n"
";TYPE:Solid infill\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:Perimeter\n"
"G1 X0 Y.2 Z.431 E0.2\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n"
";TYPE:Top solid infill\n"
"M221 S98\n"
"G1 X1 Y.3 Z.431 E0.1\n"
"M221 S95\n"
";TYPE:External perimeter\n"
"G1 X1 Y.3 Z.431 E0.1\n");
}
}
}

View File

@@ -0,0 +1,96 @@
#include <catch2/catch.hpp>
#include <memory>
#include "libslic3r/GCodeWriter.hpp"
using namespace Slic3r;
SCENARIO("lift() is not ignored after unlift() at normal values of Z", "[GCodeWriter]") {
GIVEN("A config from a file and a single extruder.") {
GCodeWriter writer;
GCodeConfig &config = writer.config;
config.load(std::string(TEST_DATA_DIR) + "/fff_print_tests/test_gcodewriter/config_lift_unlift.ini", ForwardCompatibilitySubstitutionRule::Disable);
std::vector<unsigned int> extruder_ids {0};
writer.set_extruders(extruder_ids);
writer.set_extruder(0);
WHEN("Z is set to 203") {
double trouble_Z = 203;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 500003") {
double trouble_Z = 500003;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
WHEN("Z is set to 10.3") {
double trouble_Z = 10.3;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.lift().size() > 0);
AND_WHEN("Z is moved post-lift to the same delta as the config Z lift") {
REQUIRE(writer.travel_to_z(trouble_Z + config.retract_lift.values[0]).size() == 0);
AND_WHEN("GCodeWriter::Unlift() is called") {
REQUIRE(writer.unlift().size() == 0); // we're the same height so no additional move happens.
THEN("GCodeWriter::Lift() emits gcode.") {
REQUIRE(writer.lift().size() > 0);
}
}
}
}
}
// The test above will fail for trouble_Z == 9007199254740992, where trouble_Z + 1.5 will be rounded to trouble_Z + 2.0 due to double mantisa overflow.
}
}
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
GIVEN("GCodeWriter instance") {
GCodeWriter writer;
WHEN("set_speed is called to set speed to 99999.123") {
THEN("Output string is G1 F99999.123") {
REQUIRE_THAT(writer.set_speed(99999.123), Catch::Equals("G1 F99999.123\n"));
}
}
WHEN("set_speed is called to set speed to 1") {
THEN("Output string is G1 F1") {
REQUIRE_THAT(writer.set_speed(1.0), Catch::Equals("G1 F1\n"));
}
}
WHEN("set_speed is called to set speed to 203.200022") {
THEN("Output string is G1 F203.2") {
REQUIRE_THAT(writer.set_speed(203.200022), Catch::Equals("G1 F203.2\n"));
}
}
WHEN("set_speed is called to set speed to 203.200522") {
THEN("Output string is G1 F203.201") {
REQUIRE_THAT(writer.set_speed(203.200522), Catch::Equals("G1 F203.201\n"));
}
}
}
}

View File

@@ -0,0 +1,61 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include <boost/nowide/cstdio.hpp>
#include <boost/filesystem.hpp>
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("Model construction", "[Model]") {
GIVEN("A Slic3r Model") {
Slic3r::Model model;
Slic3r::TriangleMesh sample_mesh = Slic3r::make_cube(20,20,20);
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
Slic3r::Print print;
WHEN("Model object is added") {
Slic3r::ModelObject *model_object = model.add_object();
THEN("Model object list == 1") {
REQUIRE(model.objects.size() == 1);
}
model_object->add_volume(sample_mesh);
THEN("Model volume list == 1") {
REQUIRE(model_object->volumes.size() == 1);
}
THEN("Model volume is a part") {
REQUIRE(model_object->volumes.front()->is_model_part());
}
THEN("Mesh is equivalent to input mesh.") {
REQUIRE(! sample_mesh.its.vertices.empty());
const std::vector<Vec3f>& mesh_vertices = model_object->volumes.front()->mesh().its.vertices;
Vec3f mesh_offset = model_object->volumes.front()->source.mesh_offset.cast<float>();
for (size_t i = 0; i < sample_mesh.its.vertices.size(); ++ i) {
const Vec3f &p1 = sample_mesh.its.vertices[i];
const Vec3f p2 = mesh_vertices[i] + mesh_offset;
REQUIRE((p2 - p1).norm() < EPSILON);
}
}
model_object->add_instance();
arrange_objects(model, InfiniteBed{scaled(Vec2d(100, 100))}, ArrangeParams{scaled(min_object_distance(config))});
model_object->ensure_on_bed();
print.auto_assign_extruders(model_object);
THEN("Print works?") {
print.set_status_silent();
print.apply(model, config);
print.process();
boost::filesystem::path temp = boost::filesystem::unique_path();
print.export_gcode(temp.string(), nullptr, nullptr);
REQUIRE(boost::filesystem::exists(temp));
REQUIRE(boost::filesystem::is_regular_file(temp));
REQUIRE(boost::filesystem::file_size(temp) > 0);
boost::nowide::remove(temp.string().c_str());
}
}
}
}

View File

@@ -0,0 +1,271 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std::literals;
SCENARIO("Basic tests", "[Multi]")
{
WHEN("Slicing multi-material print with non-consecutive extruders") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
{
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "extruder", 2 },
{ "infill_extruder", 4 },
{ "support_material_extruder", 0 }
});
THEN("Sliced successfully") {
REQUIRE(! gcode.empty());
}
THEN("T3 toolchange command found") {
bool T1_found = gcode.find("\nT3\n") != gcode.npos;
REQUIRE(T1_found);
}
}
WHEN("Slicing with multiple skirts with a single, non-zero extruder") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 },
{
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "perimeter_extruder", 2 },
{ "infill_extruder", 2 },
{ "support_material_extruder", 2 },
{ "support_material_interface_extruder", 2 },
});
THEN("Sliced successfully") {
REQUIRE(! gcode.empty());
}
}
}
SCENARIO("Ooze prevention", "[Multi]")
{
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "raft_layers", 2 },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 3 },
{ "support_material_extruder", 4 },
{ "ooze_prevention", 1 },
{ "extruder_offset", "0x0, 20x0, 0x20, 20x20" },
{ "temperature", "200, 180, 170, 160" },
{ "first_layer_temperature", "206, 186, 166, 156" },
// test that it doesn't crash when this is supplied
{ "toolchange_gcode", "T[next_extruder] ;toolchange" }
});
FullPrintConfig print_config;
print_config.apply(config);
// Since July 2019, QIDISlicer only emits automatic Tn command in case that the toolchange_gcode is empty
// The "T[next_extruder]" is therefore needed in this test.
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
GCodeReader parser;
int tool = -1;
int tool_temp[] = { 0, 0, 0, 0};
Points toolchange_points;
Points extrusion_points;
parser.parse_buffer(gcode, [&tool, &tool_temp, &toolchange_points, &extrusion_points, &print_config]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
// Ignore initial toolchange.
if (tool != -1) {
int expected_temp = is_approx<double>(self.z(), print_config.get_abs_value("first_layer_height") + print_config.z_offset) ?
print_config.first_layer_temperature.get_at(tool) :
print_config.temperature.get_at(tool);
if (tool_temp[tool] != expected_temp + print_config.standby_temperature_delta)
throw std::runtime_error("Standby temperature was not set before toolchange.");
toolchange_points.emplace_back(self.xy_scaled());
}
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd_is("M104") || line.cmd_is("M109")) {
// May not be defined on this line.
int t = tool;
line.has_value('T', t);
// Should be available on this line.
int s;
if (! line.has_value('S', s))
throw std::runtime_error("M104 or M109 without S");
// Following is obsolete. The first printing extruder is newly set to its first layer temperature immediately, not to the standby.
//if (tool_temp[t] == 0 && s != print_config.first_layer_temperature.get_at(t) + print_config.standby_temperature_delta)
// throw std::runtime_error("initial temperature is not equal to first layer temperature + standby delta");
tool_temp[t] = s;
} else if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0) {
extrusion_points.emplace_back(line.new_XY_scaled(self) + scaled<coord_t>(print_config.extruder_offset.get_at(tool)));
}
});
Polygon convex_hull = Geometry::convex_hull(extrusion_points);
// THEN("all nozzles are outside skirt at toolchange") {
// Points t;
// sort_remove_duplicates(toolchange_points);
// size_t inside = 0;
// for (const auto &point : toolchange_points)
// for (const Vec2d &offset : print_config.extruder_offset.values) {
// Point p = point + scaled<coord_t>(offset);
// if (convex_hull.contains(p))
// ++ inside;
// }
// REQUIRE(inside == 0);
// }
#if 0
require "Slic3r/SVG.pm";
Slic3r::SVG::output(
"ooze_prevention_test.svg",
no_arrows => 1,
polygons => [$convex_hull],
red_points => \@t,
points => \@toolchange_points,
);
#endif
THEN("all toolchanges happen within expected area") {
// offset the skirt by the maximum displacement between extruders plus a safety extra margin
const float delta = scaled<float>(20. * sqrt(2.) + 1.);
Polygon outer_convex_hull = expand(convex_hull, delta).front();
size_t inside = std::count_if(toolchange_points.begin(), toolchange_points.end(), [&outer_convex_hull](const Point &p){ return outer_convex_hull.contains(p); });
REQUIRE(inside == toolchange_points.size());
}
}
std::string slice_stacked_cubes(const DynamicPrintConfig &config, const DynamicPrintConfig &volume1config, const DynamicPrintConfig &volume2config)
{
Model model;
ModelObject *object = model.add_object();
object->name = "object.stl";
ModelVolume *v1 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
v1->set_material_id("lower_material");
v1->config.assign_config(volume1config);
ModelVolume *v2 = object->add_volume(Test::mesh(Test::TestMesh::cube_20x20x20));
v2->set_material_id("upper_material");
v2->translate(0., 0., 20.);
v2->config.assign_config(volume2config);
object->add_instance();
object->ensure_on_bed();
Print print;
print.auto_assign_extruders(object);
THEN("auto_assign_extruders() assigned correct extruder to first volume") {
REQUIRE(v1->config.extruder() == 1);
}
THEN("auto_assign_extruders() assigned correct extruder to second volume") {
REQUIRE(v2->config.extruder() == 2);
}
print.apply(model, config);
print.validate();
return Test::gcode(print);
}
SCENARIO("Stacked cubes", "[Multi]")
{
DynamicPrintConfig lower_config;
lower_config.set_deserialize_strict({
{ "extruder", 1 },
{ "bottom_solid_layers", 0 },
{ "top_solid_layers", 1 },
});
DynamicPrintConfig upper_config;
upper_config.set_deserialize_strict({
{ "extruder", 2 },
{ "bottom_solid_layers", 1 },
{ "top_solid_layers", 0 }
});
static constexpr const double solid_infill_speed = 99;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6, 0.6, 0.6, 0.6" },
{ "fill_density", 0 },
{ "solid_infill_speed", solid_infill_speed },
{ "top_solid_infill_speed", solid_infill_speed },
// for preventing speeds from being altered
{ "cooling", "0, 0, 0, 0" },
// for preventing speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test_shells = [](const std::string &gcode) {
GCodeReader parser;
int tool = -1;
// Scaled Z heights.
std::set<coord_t> T0_shells, T1_shells;
parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (is_approx<double>(line.new_F(self), solid_infill_speed * 60.) && (tool == 0 || tool == 1))
(tool == 0 ? T0_shells : T1_shells).insert(scaled<coord_t>(self.z()));
}
});
return std::make_pair(T0_shells, T1_shells);
};
WHEN("Interface shells disabled") {
std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
auto [t0, t1] = test_shells(gcode);
THEN("no interface shells") {
REQUIRE(t0.empty());
REQUIRE(t1.empty());
}
}
WHEN("Interface shells enabled") {
config.set_deserialize_strict("interface_shells", "1");
std::string gcode = slice_stacked_cubes(config, lower_config, upper_config);
auto [t0, t1] = test_shells(gcode);
THEN("top interface shells") {
REQUIRE(t0.size() == lower_config.opt_int("top_solid_layers"));
}
THEN("bottom interface shells") {
REQUIRE(t1.size() == upper_config.opt_int("bottom_solid_layers"));
}
}
WHEN("Slicing with auto-assigned extruders") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "layer_height", 0.4 },
{ "first_layer_height", 0.4 },
{ "skirts", 0 }
});
std::string gcode = slice_stacked_cubes(config, DynamicPrintConfig{}, DynamicPrintConfig{});
GCodeReader parser;
int tool = -1;
// Scaled Z heights.
std::set<coord_t> T0_shells, T1_shells;
parser.parse_buffer(gcode, [&tool, &T0_shells, &T1_shells](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0) {
if (tool == 0 && self.z() > 20)
// Layers incorrectly extruded with T0 at the top object.
T0_shells.insert(scaled<coord_t>(self.z()));
else if (tool == 1 && self.z() < 20)
// Layers incorrectly extruded with T1 at the bottom object.
T1_shells.insert(scaled<coord_t>(self.z()));
}
});
THEN("T0 is never used for upper object") {
REQUIRE(T0_shells.empty());
}
THEN("T0 is never used for lower object") {
REQUIRE(T1_shells.empty());
}
}
}

View File

@@ -0,0 +1,629 @@
#include <catch2/catch.hpp>
#include <numeric>
#include <sstream>
#include "libslic3r/Config.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/PerimeterGenerator.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/SurfaceCollection.hpp"
#include "libslic3r/libslic3r.h"
#include "test_data.hpp"
using namespace Slic3r;
SCENARIO("Perimeter nesting", "[Perimeters]")
{
struct TestData {
ExPolygons expolygons;
// expected number of loops
int total;
// expected number of external loops
int external;
// expected external perimeter
std::vector<bool> ext_order;
// expected number of internal contour loops
int cinternal;
// expected number of ccw loops
int ccw;
// expected ccw/cw order
std::vector<bool> ccw_order;
// expected nesting order
std::vector<std::vector<int>> nesting;
};
FullPrintConfig config;
auto test = [&config](const TestData &data) {
SurfaceCollection slices;
slices.append(data.expolygons, stInternal);
ExtrusionEntityCollection loops;
ExtrusionEntityCollection gap_fill;
ExPolygons fill_expolygons;
Flow flow(1., 1., 1.);
PerimeterGenerator::Parameters perimeter_generator_params(
1., // layer height
-1, // layer ID
flow, flow, flow, flow,
static_cast<const PrintRegionConfig&>(config),
static_cast<const PrintObjectConfig&>(config),
static_cast<const PrintConfig&>(config),
false); // spiral_vase
Polygons lower_layer_polygons_cache;
for (const Surface &surface : slices)
// FIXME Lukas H.: Disable this test for Arachne because it is failing and needs more investigation.
// if (config.perimeter_generator == PerimeterGeneratorType::Arachne)
// PerimeterGenerator::process_arachne();
// else
PerimeterGenerator::process_classic(
// input:
perimeter_generator_params,
surface,
nullptr,
// cache:
lower_layer_polygons_cache,
// output:
loops, gap_fill, fill_expolygons);
THEN("expected number of collections") {
REQUIRE(loops.entities.size() == data.expolygons.size());
}
loops = loops.flatten();
THEN("expected number of loops") {
REQUIRE(loops.entities.size() == data.total);
}
THEN("expected number of external loops") {
size_t num_external = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return ee->role() == ExtrusionRole::ExternalPerimeter; });
REQUIRE(num_external == data.external);
}
THEN("expected external order") {
std::vector<bool> ext_order;
for (auto *ee : loops.entities)
ext_order.emplace_back(ee->role() == ExtrusionRole::ExternalPerimeter);
REQUIRE(ext_order == data.ext_order);
}
THEN("expected number of internal contour loops") {
size_t cinternal = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->loop_role() == elrContourInternalPerimeter; });
REQUIRE(cinternal == data.cinternal);
}
THEN("expected number of ccw loops") {
size_t ccw = std::count_if(loops.entities.begin(), loops.entities.end(),
[](const ExtrusionEntity *ee){ return dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise(); });
REQUIRE(ccw == data.ccw);
}
THEN("expected ccw/cw order") {
std::vector<bool> ccw_order;
for (auto *ee : loops.entities)
ccw_order.emplace_back(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().is_counter_clockwise());
REQUIRE(ccw_order == data.ccw_order);
}
THEN("expected nesting order") {
for (const std::vector<int> &nesting : data.nesting) {
for (size_t i = 1; i < nesting.size(); ++ i)
REQUIRE(dynamic_cast<const ExtrusionLoop*>(loops.entities[nesting[i - 1]])->polygon().contains(loops.entities[nesting[i]]->first_point()));
}
}
};
WHEN("Rectangle") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }) }
};
data.total = 3;
data.external = 1;
data.ext_order = { false, false, true };
data.cinternal = 1;
data.ccw = 3;
data.ccw_order = { true, true, true };
data.nesting = { { 2, 1, 0 } };
test(data);
}
WHEN("Rectangle with hole") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {100,0}, {100,100}, {0,100} }),
Polygon::new_scale({ {40,40}, {40,60}, {60,60}, {60,40} }) }
};
data.total = 6;
data.external = 2;
data.ext_order = { false, false, true, false, false, true };
data.cinternal = 1;
data.ccw = 3;
data.ccw_order = { false, false, false, true, true, true };
data.nesting = { { 5, 4, 3, 0, 1, 2 } };
test(data);
}
WHEN("Nested rectangles with holes") {
config.perimeters.value = 3;
TestData data;
data.expolygons = {
ExPolygon{ Polygon::new_scale({ {0,0}, {200,0}, {200,200}, {0,200} }),
Polygon::new_scale({ {20,20}, {20,180}, {180,180}, {180,20} }) },
ExPolygon{ Polygon::new_scale({ {50,50}, {150,50}, {150,150}, {50,150} }),
Polygon::new_scale({ {80,80}, {80,120}, {120,120}, {120,80} }) }
};
data.total = 4*3;
data.external = 4;
data.ext_order = { false, false, true, false, false, true, false, false, true, false, false, true };
data.cinternal = 2;
data.ccw = 2*3;
data.ccw_order = { false, false, false, true, true, true, false, false, false, true, true, true };
test(data);
}
WHEN("Rectangle with multiple holes") {
config.perimeters.value = 2;
TestData data;
ExPolygon expoly{ Polygon::new_scale({ {0,0}, {50,0}, {50,50}, {0,50} }) };
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,7.5}, {7.5,12.5}, {12.5,12.5}, {12.5,7.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,17.5}, {7.5,22.5}, {12.5,22.5}, {12.5,17.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,27.5}, {7.5,32.5}, {12.5,32.5}, {12.5,27.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {7.5,37.5}, {7.5,42.5}, {12.5,42.5}, {12.5,37.5} }));
expoly.holes.emplace_back(Polygon::new_scale({ {17.5,7.5}, {17.5,12.5}, {22.5,12.5}, {22.5,7.5} }));
data.expolygons = { expoly };
data.total = 12;
data.external = 6;
data.ext_order = { false, true, false, true, false, true, false, true, false, true, false, true };
data.cinternal = 1;
data.ccw = 2;
data.ccw_order = { false, false, false, false, false, false, false, false, false, false, true, true };
data.nesting = { {0,1},{2,3},{4,5},{6,7},{8,9} };
test(data);
};
}
SCENARIO("Perimeters", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "fill_density", 0 },
{ "perimeters", 3 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
WHEN("Bridging perimeters disabled") {
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::overhang }, config);
THEN("all perimeters extruded ccw") {
GCodeReader parser;
bool has_cw_loops = false;
Polygon current_loop;
parser.parse_buffer(gcode, [&has_cw_loops, &current_loop](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (current_loop.empty())
current_loop.points.emplace_back(self.xy_scaled());
current_loop.points.emplace_back(line.new_XY_scaled(self));
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
if (! current_loop.empty() && current_loop.is_clockwise())
has_cw_loops = true;
current_loop.clear();
}
});
REQUIRE(! has_cw_loops);
}
}
auto test = [&config](Test::TestMesh model) {
// we test two copies to make sure ExtrusionLoop objects are not modified in-place (the second object would not detect cw loops and thus would calculate wrong)
std::string gcode = Slic3r::Test::slice({ model, model }, config);
GCodeReader parser;
bool has_cw_loops = false;
bool has_outwards_move = false;
bool starts_on_convex_point = false;
// print_z => count of external loops
std::map<coord_t, int> external_loops;
Polygon current_loop;
const double external_perimeter_speed = config.get_abs_value("external_perimeter_speed") * 60.;
parser.parse_buffer(gcode, [&has_cw_loops, &has_outwards_move, &starts_on_convex_point, &external_loops, &current_loop, external_perimeter_speed, model]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0) {
if (current_loop.empty())
current_loop.points.emplace_back(self.xy_scaled());
current_loop.points.emplace_back(line.new_XY_scaled(self));
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
if (! current_loop.empty()) {
if (current_loop.is_clockwise())
has_cw_loops = true;
if (is_approx<double>(self.f(), external_perimeter_speed)) {
// reset counter for second object
coord_t z = scaled<coord_t>(self.z());
auto it = external_loops.find(z);
if (it == external_loops.end())
it = external_loops.insert(std::make_pair(z, 0)).first;
else if (it->second == 2)
it->second = 0;
++ it->second;
bool is_contour = it->second == 2;
bool is_hole = it->second == 1;
// Testing whether the move point after loop ends up inside the extruded loop.
bool loop_contains_point = current_loop.contains(line.new_XY_scaled(self));
if (// contour should include destination
(! loop_contains_point && is_contour) ||
// hole should not
(loop_contains_point && is_hole))
has_outwards_move = true;
if (model == Test::TestMesh::cube_with_concave_hole) {
// check that loop starts at a concave vertex
double cross = cross2((current_loop.points.front() - current_loop.points[current_loop.points.size() - 2]).cast<double>(), (current_loop.points[1] - current_loop.points.front()).cast<double>());
bool convex = cross > 0.;
if ((convex && is_contour) || (! convex && is_hole))
starts_on_convex_point = true;
}
}
current_loop.clear();
}
}
});
THEN("all perimeters extruded ccw") {
REQUIRE(! has_cw_loops);
}
// FIXME Lukas H.: Arachne is printing external loops before hole loops in this test case.
if (config.opt_enum<PerimeterGeneratorType>("perimeter_generator") == Slic3r::PerimeterGeneratorType::Arachne) {
THEN("move outwards after completing external loop") {
// REQUIRE(! has_outwards_move);
}
// FIXME Lukas H.: Disable this test for Arachne because it is failing and needs more investigation.
THEN("loops start on concave point if any") {
// REQUIRE(! starts_on_convex_point);
}
} else {
THEN("move inwards after completing external loop") {
REQUIRE(! has_outwards_move);
}
THEN("loops start on concave point if any") {
REQUIRE(! starts_on_convex_point);
}
}
};
// Reusing the config above.
config.set_deserialize_strict({
{ "external_perimeter_speed", 68 }
});
GIVEN("Cube with hole") { test(Test::TestMesh::cube_with_hole); }
GIVEN("Cube with concave hole") { test(Test::TestMesh::cube_with_concave_hole); }
WHEN("Bridging perimeters enabled") {
// Reusing the config above.
config.set_deserialize_strict({
{ "perimeters", 1 },
{ "perimeter_speed", 77 },
{ "external_perimeter_speed", 66 },
{ "enable_dynamic_overhang_speeds", false },
{ "bridge_speed", 99 },
{ "cooling", "1" },
{ "fan_below_layer_time", "0" },
{ "slowdown_below_layer_time", "0" },
{ "bridge_fan_speed", "100" },
// arbitrary value
{ "bridge_flow_ratio", 33 },
{ "overhangs", true }
});
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::overhang) }, config);
THEN("Bridging is applied to bridging perimeters") {
GCodeReader parser;
// print Z => speeds
std::map<coord_t, std::set<double>> layer_speeds;
int fan_speed = 0;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60.;
const double external_perimeter_speed = config.get_abs_value("external_perimeter_speed") * 60.;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
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");
parser.parse_buffer(gcode, [&layer_speeds, &fan_speed, perimeter_speed, external_perimeter_speed, bridge_speed, nozzle_dmr, filament_dmr, bridge_mm_per_mm]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("M107"))
fan_speed = 0;
else if (line.cmd_is("M106"))
line.has_value('S', fan_speed);
else if (line.extruding(self) && line.dist_XY(self) > 0) {
double feedrate = line.new_F(self);
REQUIRE((is_approx(feedrate, perimeter_speed) || is_approx(feedrate, external_perimeter_speed) || is_approx(feedrate, bridge_speed)));
layer_speeds[self.z()].insert(feedrate);
bool bridging = is_approx(feedrate, bridge_speed);
double mm_per_mm = line.dist_E(self) / line.dist_XY(self);
// Fan enabled at full speed when bridging, disabled when not bridging.
REQUIRE((! bridging || fan_speed == 255));
REQUIRE((bridging || fan_speed == 0));
// When bridging, bridge flow is applied.
REQUIRE((! bridging || std::abs(mm_per_mm - bridge_mm_per_mm) <= 0.01));
}
});
// only overhang layer has more than one speed
size_t num_overhangs = std::count_if(layer_speeds.begin(), layer_speeds.end(), [](const std::pair<double, std::set<double>> &v){ return v.second.size() > 1; });
REQUIRE(num_overhangs == 1);
}
}
GIVEN("iPad stand") {
WHEN("Extra perimeters enabled") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "layer_height", 0.4 },
{ "first_layer_height", 0.35 },
{ "extra_perimeters", 1 },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
{ "perimeter_speed", 99 },
{ "external_perimeter_speed", 99 },
{ "small_perimeter_speed", 99 },
{ "thin_walls", 0 },
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::ipadstand }, config);
// z => number of loops
std::map<coord_t, int> perimeters;
bool in_loop = false;
const double perimeter_speed = config.opt_float("perimeter_speed") * 60.;
GCodeReader parser;
parser.parse_buffer(gcode, [&perimeters, &in_loop, perimeter_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), perimeter_speed)) {
if (! in_loop) {
coord_t z = scaled<coord_t>(self.z());
auto it = perimeters.find(z);
if (it == perimeters.end())
it = perimeters.insert(std::make_pair(z, 0)).first;
++ it->second;
}
in_loop = true;
} else if (! line.cmd_is("M73")) {
// skips remaining time lines (M73)
in_loop = false;
}
});
THEN("no superfluous extra perimeters") {
const int num_perimeters = config.opt_int("perimeters");
size_t extra_perimeters = std::count_if(perimeters.begin(), perimeters.end(), [num_perimeters](const std::pair<const coord_t, int> &v){ return (v.second % num_perimeters) > 0; });
REQUIRE(extra_perimeters == 0);
}
}
}
}
SCENARIO("Some weird coverage test", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", "0.4" },
{ "perimeters", 2 },
{ "perimeter_extrusion_width", 0.4 },
{ "external_perimeter_extrusion_width", 0.4 },
{ "infill_extrusion_width", 0.53 },
{ "solid_infill_extrusion_width", 0.53 }
});
// we just need a pre-filled Print object
Print print;
Model model;
Slic3r::Test::init_print({ Test::TestMesh::cube_20x20x20 }, print, model, config);
// override a layer's slices
ExPolygon expolygon;
expolygon.contour = {
{-71974463,-139999376},{-71731792,-139987456},{-71706544,-139985616},{-71682119,-139982639},{-71441248,-139946912},{-71417487,-139942895},{-71379384,-139933984},{-71141800,-139874480},
{-71105247,-139862895},{-70873544,-139779984},{-70838592,-139765856},{-70614943,-139660064},{-70581783,-139643567},{-70368368,-139515680},{-70323751,-139487872},{-70122160,-139338352},
{-70082399,-139306639},{-69894800,-139136624},{-69878679,-139121327},{-69707992,-138933008},{-69668575,-138887343},{-69518775,-138685359},{-69484336,-138631632},{-69356423,-138418207},
{-69250040,-138193296},{-69220920,-138128976},{-69137992,-137897168},{-69126095,-137860255},{-69066568,-137622608},{-69057104,-137582511},{-69053079,-137558751},{-69017352,-137317872},
{-69014392,-137293456},{-69012543,-137268207},{-68999369,-137000000},{-63999999,-137000000},{-63705947,-136985551},{-63654984,-136977984},{-63414731,-136942351},{-63364756,-136929840},
{-63129151,-136870815},{-62851950,-136771631},{-62585807,-136645743},{-62377483,-136520895},{-62333291,-136494415},{-62291908,-136463728},{-62096819,-136319023},{-62058644,-136284432},
{-61878676,-136121328},{-61680968,-135903184},{-61650275,-135861807},{-61505591,-135666719},{-61354239,-135414191},{-61332211,-135367615},{-61228359,-135148063},{-61129179,-134870847},
{-61057639,-134585262},{-61014451,-134294047},{-61000000,-134000000},{-61000000,-107999999},{-61014451,-107705944},{-61057639,-107414736},{-61129179,-107129152},{-61228359,-106851953},
{-61354239,-106585808},{-61505591,-106333288},{-61680967,-106096816},{-61878675,-105878680},{-62096820,-105680967},{-62138204,-105650279},{-62333292,-105505591},{-62585808,-105354239},
{-62632384,-105332207},{-62851951,-105228360},{-62900463,-105211008},{-63129152,-105129183},{-63414731,-105057640},{-63705947,-105014448},{-63999999,-105000000},{-68999369,-105000000},
{-69012543,-104731792},{-69014392,-104706544},{-69017352,-104682119},{-69053079,-104441248},{-69057104,-104417487},{-69066008,-104379383},{-69125528,-104141799},{-69137111,-104105248},
{-69220007,-103873544},{-69234136,-103838591},{-69339920,-103614943},{-69356415,-103581784},{-69484328,-103368367},{-69512143,-103323752},{-69661647,-103122160},{-69693352,-103082399},
{-69863383,-102894800},{-69878680,-102878679},{-70066999,-102707992},{-70112656,-102668576},{-70314648,-102518775},{-70368367,-102484336},{-70581783,-102356424},{-70806711,-102250040},
{-70871040,-102220919},{-71102823,-102137992},{-71139752,-102126095},{-71377383,-102066568},{-71417487,-102057104},{-71441248,-102053079},{-71682119,-102017352},{-71706535,-102014392},
{-71731784,-102012543},{-71974456,-102000624},{-71999999,-102000000},{-104000000,-102000000},{-104025536,-102000624},{-104268207,-102012543},{-104293455,-102014392},
{-104317880,-102017352},{-104558751,-102053079},{-104582512,-102057104},{-104620616,-102066008},{-104858200,-102125528},{-104894751,-102137111},{-105126455,-102220007},
{-105161408,-102234136},{-105385056,-102339920},{-105418215,-102356415},{-105631632,-102484328},{-105676247,-102512143},{-105877839,-102661647},{-105917600,-102693352},
{-106105199,-102863383},{-106121320,-102878680},{-106292007,-103066999},{-106331424,-103112656},{-106481224,-103314648},{-106515663,-103368367},{-106643575,-103581783},
{-106749959,-103806711},{-106779080,-103871040},{-106862007,-104102823},{-106873904,-104139752},{-106933431,-104377383},{-106942896,-104417487},{-106946920,-104441248},
{-106982648,-104682119},{-106985607,-104706535},{-106987456,-104731784},{-107000630,-105000000},{-112000000,-105000000},{-112294056,-105014448},{-112585264,-105057640},
{-112870848,-105129184},{-112919359,-105146535},{-113148048,-105228360},{-113194624,-105250392},{-113414191,-105354239},{-113666711,-105505591},{-113708095,-105536279},
{-113903183,-105680967},{-114121320,-105878679},{-114319032,-106096816},{-114349720,-106138200},{-114494408,-106333288},{-114645760,-106585808},{-114667792,-106632384},
{-114771640,-106851952},{-114788991,-106900463},{-114870815,-107129151},{-114942359,-107414735},{-114985551,-107705943},{-115000000,-107999999},{-115000000,-134000000},
{-114985551,-134294048},{-114942359,-134585263},{-114870816,-134870847},{-114853464,-134919359},{-114771639,-135148064},{-114645759,-135414192},{-114494407,-135666720},
{-114319031,-135903184},{-114121320,-136121327},{-114083144,-136155919},{-113903184,-136319023},{-113861799,-136349712},{-113666711,-136494416},{-113458383,-136619264},
{-113414192,-136645743},{-113148049,-136771631},{-112870848,-136870815},{-112820872,-136883327},{-112585264,-136942351},{-112534303,-136949920},{-112294056,-136985551},
{-112000000,-137000000},{-107000630,-137000000},{-106987456,-137268207},{-106985608,-137293440},{-106982647,-137317872},{-106946920,-137558751},{-106942896,-137582511},
{-106933991,-137620624},{-106874471,-137858208},{-106862888,-137894751},{-106779992,-138126463},{-106765863,-138161424},{-106660080,-138385055},{-106643584,-138418223},
{-106515671,-138631648},{-106487855,-138676256},{-106338352,-138877839},{-106306647,-138917600},{-106136616,-139105199},{-106121320,-139121328},{-105933000,-139291999},
{-105887344,-139331407},{-105685351,-139481232},{-105631632,-139515663},{-105418216,-139643567},{-105193288,-139749951},{-105128959,-139779072},{-104897175,-139862016},
{-104860247,-139873904},{-104622616,-139933423},{-104582511,-139942896},{-104558751,-139946912},{-104317880,-139982656},{-104293463,-139985616},{-104268216,-139987456},
{-104025544,-139999376},{-104000000,-140000000},{-71999999,-140000000}
};
expolygon.holes = {
{{-105000000,-138000000},{-105000000,-104000000},{-71000000,-104000000},{-71000000,-138000000}},
{{-69000000,-132000000},{-69000000,-110000000},{-64991180,-110000000},{-64991180,-132000000}},
{{-111008824,-132000000},{-111008824,-110000000},{-107000000,-110000000},{-107000000,-132000000}}
};
PrintObject *object = print.get_object(0);
object->slice();
Layer *layer = object->get_layer(1);
LayerRegion *layerm = layer->get_region(0);
layerm->m_slices.clear();
layerm->m_slices.append({ expolygon }, stInternal);
layer->lslices = { expolygon };
layer->lslices_ex = { { get_extents(expolygon) } };
// make perimeters
layer->make_perimeters();
// compute the covered area
Flow pflow = layerm->flow(frPerimeter);
Flow iflow = layerm->flow(frInfill);
Polygons covered_by_perimeters;
Polygons covered_by_infill;
{
Polygons acc;
for (const ExtrusionEntity *ee : layerm->perimeters())
for (const ExtrusionEntity *ee : dynamic_cast<const ExtrusionEntityCollection*>(ee)->entities)
append(acc, offset(dynamic_cast<const ExtrusionLoop*>(ee)->polygon().split_at_first_point(), float(pflow.scaled_width() / 2.f + SCALED_EPSILON)));
covered_by_perimeters = union_(acc);
}
{
Polygons acc;
for (const ExPolygon &expolygon : layerm->fill_expolygons())
append(acc, to_polygons(expolygon));
for (const ExtrusionEntity *ee : layerm->thin_fills().entities)
append(acc, offset(dynamic_cast<const ExtrusionPath*>(ee)->polyline, float(iflow.scaled_width() / 2.f + SCALED_EPSILON)));
covered_by_infill = union_(acc);
}
// compute the non covered area
ExPolygons non_covered = diff_ex(to_polygons(layerm->slices().surfaces), union_(covered_by_perimeters, covered_by_infill));
/*
if (0) {
printf "max non covered = %f\n", List::Util::max(map unscale unscale $_->area, @$non_covered);
require "Slic3r/SVG.pm";
Slic3r::SVG::output(
"gaps.svg",
expolygons => [ map $_->expolygon, @{$layerm->slices} ],
red_expolygons => union_ex([ map @$_, (@$covered_by_perimeters, @$covered_by_infill) ]),
green_expolygons => union_ex($non_covered),
no_arrows => 1,
polylines => [
map $_->polygon->split_at_first_point, map @$_, @{$layerm->perimeters},
],
);
}
*/
THEN("no gap between perimeters and infill") {
size_t num_non_convered = std::count_if(non_covered.begin(), non_covered.end(),
[&iflow](const ExPolygon &ex){ return ex.area() > sqr(double(iflow.scaled_width())); });
REQUIRE(num_non_convered == 0);
}
}
SCENARIO("Perimeters3", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 3 },
{ "layer_height", 0.15 },
{ "bridge_speed", 99 },
{ "enable_dynamic_overhang_speeds", false },
// to prevent bridging over sparse infill
{ "fill_density", 0 },
{ "overhangs", true },
// to prevent speeds from being altered
{ "cooling", "0" },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" }
});
auto test = [&config](const Vec3d &scale) {
std::string gcode = Slic3r::Test::slice({ mesh(Slic3r::Test::TestMesh::V, Vec3d::Zero(), scale) }, config);
GCodeReader parser;
std::set<coord_t> z_with_bridges;
const double bridge_speed = config.opt_float("bridge_speed") * 60.;
parser.parse_buffer(gcode, [&z_with_bridges, bridge_speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.extruding(self) && line.dist_XY(self) > 0 && is_approx<double>(line.new_F(self), bridge_speed))
z_with_bridges.insert(scaled<coord_t>(self.z()));
});
return z_with_bridges.size();
};
GIVEN("V shape, unscaled") {
int n = test(Vec3d(1., 1., 1.));
// One bridge layer under the V middle and one layer (two briding areas) under tops
THEN("no overhangs printed with bridge speed") {
REQUIRE(n == 2);
}
}
GIVEN("V shape, scaled 3x in X") {
int n = test(Vec3d(3., 1., 1.));
// except for the two internal solid layers above void
THEN("overhangs printed with bridge speed") {
REQUIRE(n > 2);
}
}
}
SCENARIO("Perimeters4", "[Perimeters]")
{
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "seam_position", "random" }
});
std::string gcode = Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config);
THEN("successful generation of G-code with seam_position = random") {
REQUIRE(! gcode.empty());
}
}
SCENARIO("Seam alignment", "[Perimeters]")
{
auto test = [](Test::TestMesh model) {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "seam_position", "aligned" },
{ "skirts", 0 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "retract_layer_change", "0" }
});
std::string gcode = Slic3r::Test::slice({ model }, config);
bool was_extruding = false;
Points seam_points;
GCodeReader parser;
parser.parse_buffer(gcode, [&was_extruding, &seam_points](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;
}
});
THEN("seam is aligned") {
size_t num_not_aligned = 0;
for (size_t i = 1; i < seam_points.size(); ++ i) {
double d = (seam_points[i] - seam_points[i - 1]).cast<double>().norm();
// Seams shall be aligned up to 3mm.
if (d > scaled<double>(3.))
++ num_not_aligned;
}
REQUIRE(num_not_aligned == 0);
}
};
GIVEN("20mm cube") {
test(Slic3r::Test::TestMesh::cube_20x20x20);
}
GIVEN("small_dorito") {
test(Slic3r::Test::TestMesh::small_dorito);
}
}

View File

@@ -0,0 +1,187 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
GIVEN("20mm cube and default config") {
WHEN("make_perimeters() is called") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { { "fill_density", 0 } });
const PrintObject &object = *print.objects().front();
THEN("67 layers exist in the model") {
REQUIRE(object.layers().size() == 66);
}
THEN("Every layer in region 0 has 1 island of perimeters") {
for (const Layer *layer : object.layers())
REQUIRE(layer->regions().front()->perimeters().size() == 1);
}
THEN("Every layer in region 0 has 3 paths in its perimeters list.") {
for (const Layer *layer : object.layers())
REQUIRE(layer->regions().front()->perimeters().items_count() == 3);
}
}
}
}
SCENARIO("Print: Skirt generation", "[Print]") {
GIVEN("20mm cube and default config") {
WHEN("Skirts is set to 2 loops") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "skirt_height", 1 },
{ "skirt_distance", 1 },
{ "skirts", 2 }
});
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces to become internal.", "[Print]") {
GIVEN("sliced 20mm cube and config with top_solid_surfaces = 2 and bottom_solid_surfaces = 1") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "top_solid_layers", 2 },
{ "bottom_solid_layers", 1 },
{ "layer_height", 0.25 }, // get a known number of layers
{ "first_layer_height", 0.25 }
});
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
// Precondition: Ensure that the model has 2 solid top layers (39, 38)
// and one solid bottom layer (0).
auto test_is_solid_infill = [&print](size_t obj_id, size_t layer_id) {
const Layer &layer = *print.objects()[obj_id]->get_layer((int)layer_id);
// iterate over all of the regions in the layer
for (const LayerRegion *region : layer.regions()) {
// for each region, iterate over the fill surfaces
for (const Surface &surface : region->fill_surfaces())
CHECK(surface.is_solid());
}
};
print.process();
test_is_solid_infill(0, 0); // should be solid
test_is_solid_infill(0, 79); // should be solid
test_is_solid_infill(0, 78); // should be solid
WHEN("Model is re-sliced with top_solid_layers == 3") {
config.set("top_solid_layers", 3);
print.apply(model, config);
print.process();
THEN("Print object does not have 0 solid bottom layers.") {
test_is_solid_infill(0, 0);
}
AND_THEN("Print object has 3 top solid layers") {
test_is_solid_infill(0, 79);
test_is_solid_infill(0, 78);
test_is_solid_infill(0, 77);
}
}
}
}
SCENARIO("Print: Brim generation", "[Print]") {
GIVEN("20mm cube and default config, 1mm first layer width") {
WHEN("Brim is set to 3mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 3 }
});
THEN("Brim Extrusion collection has 3 loops in it") {
REQUIRE(print.brim().items_count() == 3);
}
}
WHEN("Brim is set to 6mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 6 }
});
THEN("Brim Extrusion collection has 6 loops in it") {
REQUIRE(print.brim().items_count() == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_extrusion_width", 1 },
{ "brim_width", 6 },
{ "first_layer_extrusion_width", 0.5 }
});
print.process();
THEN("Brim Extrusion collection has 12 loops in it") {
REQUIRE(print.brim().items_count() == 14);
}
}
}
}
SCENARIO("Ported from Perl", "[Print]") {
GIVEN("20mm cube") {
WHEN("Print center is set to 100x100 (test framework default)") {
auto config = Slic3r::DynamicPrintConfig::full_print_config();
std::string gcode = Slic3r::Test::slice({ TestMesh::cube_20x20x20 }, config);
GCodeReader parser;
Points extrusion_points;
parser.parse_buffer(gcode, [&extrusion_points](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
if (line.cmd_is("G1") && line.extruding(self) && line.dist_XY(self) > 0)
extrusion_points.emplace_back(line.new_XY_scaled(self));
});
Vec2d center = unscaled<double>(BoundingBox(extrusion_points).center());
THEN("print is centered around print_center") {
REQUIRE(is_approx(center.x(), 100.));
REQUIRE(is_approx(center.y(), 100.));
}
}
}
GIVEN("Model with multiple objects") {
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "nozzle_diameter", { 0.4, 0.4, 0.4, 0.4 } }
});
Print print;
Model model;
Slic3r::Test::init_print({ TestMesh::cube_20x20x20 }, print, model, config);
// User sets a per-region option, also testing a deep copy of Model.
Model model2(model);
model2.objects.front()->config.set_deserialize_strict("fill_density", "100%");
WHEN("fill_density overridden") {
print.apply(model2, config);
THEN("region config inherits model object config") {
REQUIRE(print.get_print_region(0).config().fill_density == 100);
}
}
model2.objects.front()->config.erase("fill_density");
WHEN("fill_density resetted") {
print.apply(model2, config);
THEN("region config is resetted") {
REQUIRE(print.get_print_region(0).config().fill_density == 20);
}
}
WHEN("extruder is assigned") {
model2.objects.front()->config.set("extruder", 3);
model2.objects.front()->config.set("perimeter_extruder", 2);
print.apply(model2, config);
THEN("extruder setting is correctly expanded") {
REQUIRE(print.get_print_region(0).config().infill_extruder == 3);
}
THEN("extruder setting does not override explicitely specified extruders") {
REQUIRE(print.get_print_region(0).config().perimeter_extruder == 2);
}
}
}
}

View File

@@ -0,0 +1,271 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp"
#include <algorithm>
#include <boost/regex.hpp>
using namespace Slic3r;
using namespace Slic3r::Test;
boost::regex perimeters_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; perimeter");
boost::regex infill_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; infill");
boost::regex skirt_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; skirt");
SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") {
GIVEN("A default configuration and a print test object") {
WHEN("the output is executed with no support material") {
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, {
{ "layer_height", 0.2 },
{ "first_layer_height", 0.2 },
{ "first_layer_extrusion_width", 0 },
{ "gcode_comments", true },
{ "start_gcode", "" }
});
std::string gcode = Slic3r::Test::gcode(print);
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains slic3r version") {
REQUIRE(gcode.find(SLIC3R_VERSION) != std::string::npos);
}
//THEN("Exported text contains git commit id") {
// REQUIRE(gcode.find("; Git Commit") != std::string::npos);
// REQUIRE(gcode.find(SLIC3R_BUILD_ID) != std::string::npos);
//}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Exported text does not contain cooling markers (they were consumed)") {
REQUIRE(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
}
THEN("GCode preamble is emitted.") {
REQUIRE(gcode.find("G21 ; set units to millimeters") != std::string::npos);
}
THEN("Config options emitted for print config, default region config, default object config") {
REQUIRE(gcode.find("; first_layer_temperature") != std::string::npos);
REQUIRE(gcode.find("; layer_height") != std::string::npos);
REQUIRE(gcode.find("; fill_density") != std::string::npos);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("final Z height is 20mm") {
double final_z = 0.0;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
final_z = std::max<double>(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
});
REQUIRE(final_z == Approx(20.));
}
}
WHEN("output is executed with complete objects and two differently-sized meshes") {
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20,TestMesh::cube_20x20x20}, print, model, {
{ "first_layer_extrusion_width", 0 },
{ "first_layer_height", 0.3 },
{ "layer_height", 0.2 },
{ "support_material", false },
{ "raft_layers", 0 },
{ "complete_objects", true },
{ "gcode_comments", true },
{ "between_objects_gcode", "; between-object-gcode" }
});
std::string gcode = Slic3r::Test::gcode(print);
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("Between-object-gcode is emitted.") {
REQUIRE(gcode.find("; between-object-gcode") != std::string::npos);
}
THEN("final Z height is 20.1mm") {
double final_z = 0.0;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
});
REQUIRE(final_z == Approx(20.1));
}
THEN("Z height resets on object change") {
double final_z = 0.0;
bool reset = false;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) { // saw higher Z before this, now it's lower
reset = true;
} else {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
THEN("Shorter object is printed before taller object.") {
double final_z = 0.0;
bool reset = false;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) {
reset = (final_z > 20.0);
} else {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
}
WHEN("the output is executed with support material") {
std::string gcode = ::Test::slice({TestMesh::cube_20x20x20}, {
{ "first_layer_extrusion_width", 0 },
{ "support_material", true },
{ "raft_layers", 3 },
{ "gcode_comments", true }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") != std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Raft is emitted.") {
REQUIRE(gcode.find("; raft") != std::string::npos);
}
}
WHEN("the output is executed with a separate first layer extrusion width") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "first_layer_extrusion_width", "0.5" }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") != std::string::npos);
}
}
WHEN("Cooling is enabled and the fan is disabled.") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "cooling", true },
{ "disable_fan_first_layers", 5 }
});
THEN("GCode to disable fan is emitted."){
REQUIRE(gcode.find("M107") != std::string::npos);
}
}
WHEN("end_gcode exists with layer_num and layer_z") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "end_gcode", "; Layer_num [layer_num]\n; Layer_z [layer_z]" },
{ "layer_height", 0.1 },
{ "first_layer_height", 0.1 }
});
THEN("layer_num and layer_z are processed in the end gcode") {
REQUIRE(gcode.find("; Layer_num 199") != std::string::npos);
REQUIRE(gcode.find("; Layer_z 20") != std::string::npos);
}
}
WHEN("current_extruder exists in start_gcode") {
{
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "start_gcode", "; Extruder [current_extruder]" }
});
THEN("current_extruder is processed in the start gcode and set for first extruder") {
REQUIRE(gcode.find("; Extruder 0") != std::string::npos);
}
}
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_num_extruders(4);
config.set_deserialize_strict({
{ "start_gcode", "; Extruder [current_extruder]" },
{ "infill_extruder", 2 },
{ "solid_infill_extruder", 2 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 2 },
{ "support_material_interface_extruder", 2 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
THEN("current_extruder is processed in the start gcode and set for second extruder") {
REQUIRE(gcode.find("; Extruder 1") != std::string::npos);
}
}
}
WHEN("layer_num represents the layer's index from z=0") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, {
{ "complete_objects", true },
{ "gcode_comments", true },
{ "layer_gcode", ";Layer:[layer_num] ([layer_z] mm)" },
{ "layer_height", 0.1 },
{ "first_layer_height", 0.1 }
});
// End of the 1st object.
std::string token = ";Layer:199 ";
size_t pos = gcode.find(token);
THEN("First and second object last layer is emitted") {
// First object
REQUIRE(pos != std::string::npos);
pos += token.size();
REQUIRE(pos < gcode.size());
double z = 0;
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
REQUIRE(z == Approx(20.));
// Second object
pos = gcode.find(";Layer:399 ", pos);
REQUIRE(pos != std::string::npos);
pos += token.size();
REQUIRE(pos < gcode.size());
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
REQUIRE(z == Approx(20.));
}
}
}
}

View File

@@ -0,0 +1,89 @@
#include <catch2/catch.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
GIVEN("20mm cube and default initial config, initial layer height of 2mm") {
WHEN("generate_object_layers() is called for 2mm layer heights and nozzle diameter of 3mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 2 },
{ "nozzle_diameter", 3 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 10 entries") {
REQUIRE(layers.size() == 10);
}
AND_THEN("Each layer is approximately 2mm above the previous Z") {
coordf_t last = 0.0;
for (size_t i = 0; i < layers.size(); ++ i) {
REQUIRE((layers[i]->print_z - last) == Approx(2.0));
last = layers[i]->print_z;
}
}
}
WHEN("generate_object_layers() is called for 10mm layer heights and nozzle diameter of 11mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 10 },
{ "nozzle_diameter", 11 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 3 entries") {
REQUIRE(layers.size() == 3);
}
AND_THEN("Layer 0 is at 2mm") {
REQUIRE(layers.front()->print_z == Approx(2.0));
}
AND_THEN("Layer 1 is at 12mm") {
REQUIRE(layers[1]->print_z == Approx(12.0));
}
}
WHEN("generate_object_layers() is called for 15mm layer heights and nozzle diameter of 16mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 15 },
{ "nozzle_diameter", 16 }
});
SpanOfConstPtrs<Layer> layers = print.objects().front()->layers();
THEN("The output vector has 2 entries") {
REQUIRE(layers.size() == 2);
}
AND_THEN("Layer 0 is at 2mm") {
REQUIRE(layers[0]->print_z == Approx(2.0));
}
AND_THEN("Layer 1 is at 17mm") {
REQUIRE(layers[1]->print_z == Approx(17.0));
}
}
#if 0
WHEN("generate_object_layers() is called for 15mm layer heights and nozzle diameter of 5mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "first_layer_height", 2 },
{ "layer_height", 15 },
{ "nozzle_diameter", 5 }
});
const std::vector<Slic3r::Layer*> &layers = print.objects().front()->layers();
THEN("The layer height is limited to 5mm.") {
CHECK(layers.size() == 5);
coordf_t last = 2.0;
for (size_t i = 1; i < layers.size(); i++) {
REQUIRE((layers[i]->print_z - last) == Approx(5.0));
last = layers[i]->print_z;
}
}
}
#endif
}
}

View File

@@ -0,0 +1,405 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
SCENARIO("Shells", "[Shells]") {
GIVEN("20mm box") {
auto test = [](const DynamicPrintConfig &config){
std::vector<coord_t> zs;
std::set<coord_t> layers_with_solid_infill;
std::set<coord_t> layers_with_bridge_infill;
const double solid_infill_speed = config.opt_float("solid_infill_speed") * 60;
const double bridge_speed = config.opt_float("bridge_speed") * 60;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({ Slic3r::Test::TestMesh::cube_20x20x20 }, config),
[&zs, &layers_with_solid_infill, &layers_with_bridge_infill, solid_infill_speed, bridge_speed]
(Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
double z = line.new_Z(self);
REQUIRE(z >= 0);
if (z > 0) {
coord_t scaled_z = scaled<float>(z);
zs.emplace_back(scaled_z);
if (line.extruding(self) && line.dist_XY(self) > 0) {
double f = line.new_F(self);
if (std::abs(f - solid_infill_speed) < EPSILON)
layers_with_solid_infill.insert(scaled_z);
if (std::abs(f - bridge_speed) < EPSILON)
layers_with_bridge_infill.insert(scaled_z);
}
}
});
sort_remove_duplicates(zs);
auto has_solid_infill = [&layers_with_solid_infill](coord_t z) { return layers_with_solid_infill.find(z) != layers_with_solid_infill.end(); };
auto has_bridge_infill = [&layers_with_bridge_infill](coord_t z) { return layers_with_bridge_infill.find(z) != layers_with_bridge_infill.end(); };
auto has_shells = [&has_solid_infill, &has_bridge_infill, &zs](int layer_idx) { coord_t z = zs[layer_idx]; return has_solid_infill(z) || has_bridge_infill(z); };
const int bottom_solid_layers = config.opt_int("bottom_solid_layers");
const int top_solid_layers = config.opt_int("top_solid_layers");
THEN("correct number of bottom solid layers") {
for (int i = 0; i < bottom_solid_layers; ++ i)
REQUIRE(has_shells(i));
for (int i = bottom_solid_layers; i < int(zs.size() / 2); ++ i)
REQUIRE(! has_shells(i));
}
THEN("correct number of top solid layers") {
// NOTE: there is one additional layer with enusring line under the bridge layer, bridges would be otherwise anchored weakly to the perimeter.
size_t additional_ensuring_anchors = top_solid_layers > 0 ? 1 : 0;
for (int i = 0; i < top_solid_layers + additional_ensuring_anchors; ++ i)
REQUIRE(has_shells(int(zs.size()) - i - 1));
for (int i = top_solid_layers + additional_ensuring_anchors; i < int(zs.size() / 2); ++ i)
REQUIRE(! has_shells(int(zs.size()) - i - 1));
}
if (top_solid_layers > 0) {
THEN("solid infill speed is used on solid infill") {
for (int i = 0; i < top_solid_layers - 1; ++ i) {
auto z = zs[int(zs.size()) - i - 1];
REQUIRE(has_solid_infill(z));
REQUIRE(! has_bridge_infill(z));
}
}
THEN("bridge used in first solid layer over sparse infill") {
auto z = zs[int(zs.size()) - top_solid_layers];
REQUIRE(! has_solid_infill(z));
REQUIRE(has_bridge_infill(z));
}
}
};
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "skirts", 0 },
{ "perimeters", 0 },
{ "solid_infill_speed", 99 },
{ "top_solid_infill_speed", 99 },
{ "bridge_speed", 72 },
{ "first_layer_speed", "100%" },
{ "cooling", "0" }
});
WHEN("three top and bottom layers") {
// proper number of shells is applied
config.set_deserialize_strict({
{ "top_solid_layers", 3 },
{ "bottom_solid_layers", 3 }
});
test(config);
}
WHEN("zero top and bottom layers") {
// no shells are applied when both top and bottom are set to zero
config.set_deserialize_strict({
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 }
});
test(config);
}
WHEN("three top and bottom layers, zero infill") {
// proper number of shells is applied even when fill density is none
config.set_deserialize_strict({
{ "perimeters", 1 },
{ "top_solid_layers", 3 },
{ "bottom_solid_layers", 3 }
});
test(config);
}
}
}
static std::set<double> layers_with_speed(const std::string &gcode, int speed)
{
std::set<double> out;
GCodeReader parser;
parser.parse_buffer(gcode, [&out, speed](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && is_approx<double>(line.new_F(self), speed * 60.))
out.insert(self.z());
});
return out;
}
SCENARIO("Shells (from Perl)", "[Shells]") {
GIVEN("V shape, Slic3r GH #1161") {
int solid_speed = 99;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "layer_height", 0.3 },
{ "first_layer_height", 0.3 },
{ "bottom_solid_layers", 0 },
{ "top_solid_layers", 3 },
// to prevent speeds from being altered
{ "cooling", "0" },
{ "bridge_speed", solid_speed },
{ "solid_infill_speed", solid_speed },
{ "top_solid_infill_speed", solid_speed },
// to prevent speeds from being altered
{ "first_layer_speed", "100%" },
// prevent speed alteration
{ "enable_dynamic_overhang_speeds", 0 }
});
THEN("correct number of top solid shells is generated in V-shaped object") {
size_t n = 0;
for (auto z : layers_with_speed(Slic3r::Test::slice({TestMesh::V}, config), solid_speed))
if (z <= 7.2)
++ n;
REQUIRE(n == 3 + 1/*one additional layer with ensuring for bridge anchors*/);
}
}
//TODO CHECK AFTER REMOVAL OF "ensure_vertical_wall_thickness"
// GIVEN("V shape") {
// // we need to check against one perimeter because this test is calibrated
// // (shape, extrusion_width) so that perimeters cover the bottom surfaces of
// // their lower layer - the test checks that shells are not generated on the
// // above layers (thus 'across' the shadow perimeter)
// // the test is actually calibrated to leave a narrow bottom region for each
// // layer - we test that in case of fill_density = 0 such narrow shells are
// // discarded instead of grown
// int bottom_solid_layers = 3;
// auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
// { "perimeters", 1 },
// { "fill_density", 0 },
// // to prevent speeds from being altered
// { "cooling", "0" },
// // to prevent speeds from being altered
// { "first_layer_speed", "100%" },
// // prevent speed alteration
// { "enable_dynamic_overhang_speeds", 0 },
// { "layer_height", 0.4 },
// { "first_layer_height", 0.4 },
// { "extrusion_width", 0.55 },
// { "bottom_solid_layers", bottom_solid_layers },
// { "top_solid_layers", 0 },
// { "solid_infill_speed", 99 }
// });
// THEN("shells are not propagated across perimeters of the neighbor layer") {
// std::string gcode = Slic3r::Test::slice({TestMesh::V}, config);
// REQUIRE(layers_with_speed(gcode, 99).size() == bottom_solid_layers);
// }
// }
// GIVEN("sloping_hole") {
// int bottom_solid_layers = 3;
// int top_solid_layers = 3;
// int solid_speed = 99;
// auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
// { "perimeters", 3 },
// // to prevent speeds from being altered
// { "cooling", "0" },
// // to prevent speeds from being altered
// { "first_layer_speed", "100%" },
// // prevent speed alteration
// { "enable_dynamic_overhang_speeds", 0 },
// { "layer_height", 0.4 },
// { "first_layer_height", 0.4 },
// { "bottom_solid_layers", bottom_solid_layers },
// { "top_solid_layers", top_solid_layers },
// { "solid_infill_speed", solid_speed },
// { "top_solid_infill_speed", solid_speed },
// { "bridge_speed", solid_speed },
// { "filament_diameter", 3. },
// { "nozzle_diameter", 0.5 }
// });
// THEN("no superfluous shells are generated") {
// std::string gcode = Slic3r::Test::slice({TestMesh::sloping_hole}, config);
// REQUIRE(layers_with_speed(gcode, solid_speed).size() == bottom_solid_layers + top_solid_layers);
// }
// }
GIVEN("20mm_cube, spiral vase") {
double layer_height = 0.3;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "layer_height", layer_height },
{ "first_layer_height", layer_height },
{ "top_solid_layers", 0 },
{ "spiral_vase", 1 },
{ "bottom_solid_layers", 0 },
{ "skirts", 0 },
{ "start_gcode", "" },
{ "temperature", 200 },
{ "first_layer_temperature", 205}
});
// TODO: this needs to be tested with a model with sloping edges, where starting
// points of each layer are not aligned - in that case we would test that no
// travel moves are left to move to the new starting point - in a cube, end
// points coincide with next layer starting points (provided there's no clipping)
auto test = [layer_height](const DynamicPrintConfig &config) {
size_t travel_moves_after_first_extrusion = 0;
bool started_extruding = false;
bool first_layer_temperature_set = false;
bool temperature_set = false;
std::vector<double> z_steps;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({TestMesh::cube_20x20x20}, config),
[&](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1")) {
if (line.extruding(self))
started_extruding = true;
if (started_extruding) {
if (double dz = line.dist_Z(self); dz > 0)
z_steps.emplace_back(dz);
if (line.travel() && line.dist_XY(self) > 0 && ! line.has(Z))
++ travel_moves_after_first_extrusion;
}
} else if (line.cmd_is("M104")) {
int s;
if (line.has_value('S', s)) {
if (s == 205)
first_layer_temperature_set = true;
else if (s == 200)
temperature_set = true;
}
}
});
THEN("first layer temperature is set") {
REQUIRE(first_layer_temperature_set);
}
THEN("temperature is set") {
REQUIRE(temperature_set);
}
// we allow one travel move after first extrusion: i.e. when moving to the first
// spiral point after moving to second layer (bottom layer had loop clipping, so
// we're slightly distant from the starting point of the loop)
THEN("no gaps in spiral vase") {
REQUIRE(travel_moves_after_first_extrusion <= 1);
}
THEN("no gaps in Z") {
REQUIRE(std::count_if(z_steps.begin(), z_steps.end(),
[&layer_height](auto z_step) { return z_step > layer_height + EPSILON; }) == 0);
}
};
WHEN("solid model") {
test(config);
}
WHEN("solid model with negative z-offset") {
config.set_deserialize_strict("z_offset", "-10");
test(config);
}
// Disabled because the current unreliable medial axis code doesn't always produce valid loops.
// $test->('40x10', 'hollow model with negative z-offset');
}
GIVEN("20mm_cube, spiral vase") {
double layer_height = 0.4;
auto config = Slic3r::DynamicPrintConfig::full_print_config_with({
{ "spiral_vase", 1 },
{ "perimeters", 1 },
{ "fill_density", 0 },
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 0 },
{ "retract_layer_change", 0 },
{ "skirts", 0 },
{ "layer_height", layer_height },
{ "first_layer_height", layer_height },
{ "start_gcode", "" },
// { "use_relative_e_distances", 1}
});
config.validate();
std::vector<std::pair<double, double>> this_layer; // [ dist_Z, dist_XY ], ...
int z_moves = 0;
bool bottom_layer_not_flat = false;
bool null_z_moves_not_layer_changes = false;
bool null_z_moves_not_multiples_of_layer_height = false;
bool sum_of_partial_z_equals_to_layer_height = false;
bool all_layer_segments_have_same_slope = false;
bool horizontal_extrusions = false;
GCodeReader parser;
parser.parse_buffer(Slic3r::Test::slice({TestMesh::cube_20x20x20}, config),
[&](Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1")) {
if (z_moves < 2) {
// skip everything up to the second Z move
// (i.e. start of second layer)
if (line.has(Z)) {
++ z_moves;
if (double dz = line.dist_Z(self); dz > 0 && ! is_approx<double>(dz, layer_height))
bottom_layer_not_flat = true;
}
} else if (line.dist_Z(self) == 0 && line.has(Z)) {
if (line.dist_XY(self) != 0)
null_z_moves_not_layer_changes = true;
double z = line.new_Z(self);
if (fmod(z + EPSILON, layer_height) > 2 * EPSILON)
null_z_moves_not_multiples_of_layer_height = true;
double total_dist_XY = 0;
double total_dist_Z = 0;
for (auto &seg : this_layer) {
total_dist_Z += seg.first;
total_dist_XY += seg.second;
}
if (std::abs(total_dist_Z - layer_height) >
// The first segment on the 2nd layer has extrusion interpolated from zero
// and the 1st segment has such a low extrusion assigned, that it is effectively zero, thus the move
// is considered non-extruding and a higher epsilon is required.
(z_moves == 2 ? 0.0021 : EPSILON))
sum_of_partial_z_equals_to_layer_height = true;
//printf("Total height: %f, layer height: %f, good: %d\n", sum(map $_->[0], @this_layer), $config->layer_height, $sum_of_partial_z_equals_to_layer_height);
for (auto &seg : this_layer)
// check that segment's dist_Z is proportioned to its dist_XY
if (std::abs(seg.first * total_dist_XY / layer_height - seg.second) > 0.2)
all_layer_segments_have_same_slope = true;
this_layer.clear();
} else if (line.extruding(self) && line.dist_XY(self) > 0) {
if (line.dist_Z(self) == 0)
horizontal_extrusions = true;
//printf("Pushing dist_z: %f, dist_xy: %f\n", $info->{dist_Z}, $info->{dist_XY});
this_layer.emplace_back(line.dist_Z(self), line.dist_XY(self));
}
}
});
THEN("bottom layer is flat when using spiral vase") {
REQUIRE(! bottom_layer_not_flat);
}
THEN("null Z moves are layer changes") {
REQUIRE(! null_z_moves_not_layer_changes);
}
THEN("null Z moves are multiples of layer height") {
REQUIRE(! null_z_moves_not_multiples_of_layer_height);
}
THEN("sum of partial Z increments equals to a full layer height") {
REQUIRE(! sum_of_partial_z_equals_to_layer_height);
}
THEN("all layer segments have the same slope") {
REQUIRE(! all_layer_segments_have_same_slope);
}
THEN("no horizontal extrusions") {
REQUIRE(! horizontal_extrusions);
}
}
}
#if 0
// The current Spiral Vase slicing code removes the holes and all but the largest contours from each slice,
// therefore the following test is no more valid.
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('perimeters', 1);
$config->set('fill_density', 0);
$config->set('top_solid_layers', 0);
$config->set('spiral_vase', 1);
$config->set('bottom_solid_layers', 0);
$config->set('skirts', 0);
$config->set('first_layer_height', $config->layer_height);
$config->set('start_gcode', '');
my $print = Slic3r::Test::init_print('two_hollow_squares', config => $config);
my $diagonal_moves = 0;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd eq 'G1') {
if ($info->{extruding} && $info->{dist_XY} > 0) {
if ($info->{dist_Z} > 0) {
$diagonal_moves++;
}
}
}
});
is $diagonal_moves, 0, 'no spiral moves on two-island object';
}
#endif

View File

@@ -0,0 +1,268 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include <boost/algorithm/string.hpp>
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
/// Helper method to find the tool used for the brim (always the first extrusion)
static int get_brim_tool(const std::string &gcode)
{
int brim_tool = -1;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the the current tool
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) {
brim_tool = tool;
}
});
return brim_tool;
}
TEST_CASE("Skirt height is honored", "[Skirt]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "skirts", 1 },
{ "skirt_height", 5 },
{ "perimeters", 0 },
{ "support_material_speed", 99 },
// avoid altering speeds unexpectedly
{ "cooling", false },
// avoid altering speeds unexpectedly
{ "first_layer_speed", "100%" }
});
std::string gcode;
SECTION("printing a single object") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
}
SECTION("printing multiple objects") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, config);
}
std::map<double, bool> layers_with_skirt;
double support_speed = config.opt<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
GCodeReader parser;
parser.parse_buffer(gcode, [&layers_with_skirt, &support_speed] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
layers_with_skirt[self.z()] = 1;
}
});
REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height"));
}
SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim]") {
GIVEN("A default configuration") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_num_extruders(4);
config.set_deserialize_strict({
{ "support_material_speed", 99 },
{ "first_layer_height", 0.3 },
{ "gcode_comments", true },
// avoid altering speeds unexpectedly
{ "cooling", false },
{ "first_layer_speed", "100%" },
// remove noise from top/solid layers
{ "top_solid_layers", 0 },
{ "bottom_solid_layers", 1 },
{ "start_gcode", "T[initial_tool]\n" }
});
WHEN("Brim width is set to 5") {
config.set_deserialize_strict({
{ "perimeters", 0 },
{ "skirts", 0 },
{ "brim_width", 5 }
});
THEN("Brim is generated") {
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
bool brim_generated = false;
double support_speed = config.opt<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
Slic3r::GCodeReader parser;
parser.parse_buffer(gcode, [&brim_generated, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
if (self.z() == Approx(0.3) || line.new_Z(self) == Approx(0.3)) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
brim_generated = true;
}
}
});
REQUIRE(brim_generated);
}
}
WHEN("Skirt area is smaller than the brim") {
config.set_deserialize_strict({
{ "skirts", 1 },
{ "brim_width", 10}
});
THEN("Gcode generates") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
WHEN("Skirt height is 0 and skirts > 0") {
config.set_deserialize_strict({
{ "skirts", 2 },
{ "skirt_height", 0 }
});
THEN("Gcode generates") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
#if 0
// This is a real error! One shall print the brim with the external perimeter extruder!
WHEN("Perimeter extruder = 2 and support extruders = 3") {
THEN("Brim is printed with the extruder used for the perimeters of first object") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("perimeter_extruder") - 1);
}
}
WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") {
THEN("brim is printed with same extruder as skirt") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 },
{ "raft_layers", 1 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("support_material_extruder") - 1);
}
}
#endif
WHEN("brim width to 1 with layer_width of 0.5") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 }
});
THEN("2 brim lines") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
REQUIRE(print.brim().entities.size() == 2);
}
}
#if 0
WHEN("brim ears on a square") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 91 }
});
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
THEN("Four brim ears") {
REQUIRE(print.brim().entities.size() == 4);
}
}
WHEN("brim ears on a square but with a too small max angle") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 89 }
});
THEN("no brim") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, config);
REQUIRE(print.brim().entities.size() == 0);
}
}
#endif
WHEN("Object is plated with overhang support and a brim") {
config.set_deserialize_strict({
{ "layer_height", 0.4 },
{ "first_layer_height", 0.4 },
{ "skirts", 1 },
{ "skirt_distance", 0 },
{ "support_material_speed", 99 },
{ "perimeter_extruder", 1 },
{ "support_material_extruder", 2 },
{ "infill_extruder", 3 }, // ensure that a tool command gets emitted.
{ "cooling", false }, // to prevent speeds to be altered
{ "first_layer_speed", "100%" }, // to prevent speeds to be altered
{ "start_gcode", "T[initial_tool]\n" }
});
THEN("overhang generates?") {
//FIXME does it make sense?
REQUIRE(! Slic3r::Test::slice({TestMesh::overhang}, config).empty());
}
// config.set("support_material", true); // to prevent speeds to be altered
#if 0
// This test is not finished.
THEN("skirt length is large enough to contain object with support") {
CHECK(config.opt_bool("support_material")); // test is not valid if support material is off
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
double support_speed = config.opt<ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
double skirt_length = 0.0;
Points extrusion_points;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [config, &extrusion_points, &tool, &skirt_length, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
// std::cerr << line.cmd() << "\n";
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (self.z() == Approx(config.opt<ConfigOptionFloat>("first_layer_height")->value)) {
// on first layer
if (line.extruding(self) && line.dist_XY(self) > 0) {
float speed = ( self.f() > 0 ? self.f() : line.new_F(self));
// std::cerr << "Tool " << tool << "\n";
if (speed == Approx(support_speed) && tool == config.opt_int("perimeter_extruder") - 1) {
// Skirt uses first material extruder, support material speed.
skirt_length += line.dist_XY(self);
} else
extrusion_points.push_back(Slic3r::Point::new_scale(line.new_X(self), line.new_Y(self)));
}
}
if (self.z() == Approx(0.3) || line.new_Z(self) == Approx(0.3)) {
if (line.extruding(self) && self.f() == Approx(support_speed)) {
}
}
});
Slic3r::Polygon convex_hull = Slic3r::Geometry::convex_hull(extrusion_points);
double hull_perimeter = unscale<double>(convex_hull.split_at_first_point().length());
REQUIRE(skirt_length > hull_perimeter);
}
#endif
}
WHEN("Large minimum skirt length is used.") {
config.set("min_skirt_length", 20);
THEN("Gcode generation doesn't crash") {
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
}
}
}
}

View File

@@ -0,0 +1,497 @@
#include <catch2/catch.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Layer.hpp"
#include "test_data.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
TEST_CASE("SupportMaterial: Three raft layers created", "[SupportMaterial]")
{
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, {
{ "support_material", 1 },
{ "raft_layers", 3 }
});
REQUIRE(print.objects().front()->support_layers().size() == 3);
}
SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMaterial]")
{
// Box h = 20mm, hole bottom at 5mm, hole height 10mm (top edge at 15mm).
TriangleMesh mesh = Slic3r::Test::mesh(Slic3r::Test::TestMesh::cube_with_hole);
mesh.rotate_x(float(M_PI / 2));
// mesh.write_binary("d:\\temp\\cube_with_hole.stl");
auto check = [](Slic3r::Print &print, bool &first_support_layer_height_ok, bool &layer_height_minimum_ok, bool &layer_height_maximum_ok, bool &top_spacing_ok)
{
SpanOfConstPtrs<SupportLayer> support_layers = print.objects().front()->support_layers();
first_support_layer_height_ok = support_layers.front()->print_z == print.config().first_layer_height.value;
layer_height_minimum_ok = true;
layer_height_maximum_ok = true;
double min_layer_height = print.config().min_layer_height.values.front();
double max_layer_height = print.config().nozzle_diameter.values.front();
if (print.config().max_layer_height.values.front() > EPSILON)
max_layer_height = std::min(max_layer_height, print.config().max_layer_height.values.front());
for (size_t i = 1; i < support_layers.size(); ++ i) {
if (support_layers[i]->print_z - support_layers[i - 1]->print_z < min_layer_height - EPSILON)
layer_height_minimum_ok = false;
if (support_layers[i]->print_z - support_layers[i - 1]->print_z > max_layer_height + EPSILON)
layer_height_maximum_ok = false;
}
#if 0
double expected_top_spacing = print.default_object_config().layer_height + print.config().nozzle_diameter.get_at(0);
bool wrong_top_spacing = 0;
std::vector<coordf_t> top_z { 1.1 };
for (coordf_t top_z_el : top_z) {
// find layer index of this top surface.
size_t layer_id = -1;
for (size_t i = 0; i < support_z.size(); ++ i) {
if (abs(support_z[i] - top_z_el) < EPSILON) {
layer_id = i;
i = static_cast<int>(support_z.size());
}
}
// check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
if (abs(support_z[layer_id + 1] - support_z[layer_id] - expected_top_spacing) > EPSILON &&
abs(support_z[layer_id + 2] - support_z[layer_id] - expected_top_spacing) > EPSILON) {
wrong_top_spacing = 1;
}
}
d = ! wrong_top_spacing;
#else
top_spacing_ok = true;
#endif
};
GIVEN("A print object having one modelObject") {
WHEN("First layer height = 0.4") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.4 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
WHEN("Layer height = 0.2 and, first layer height = 0.3") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.3 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
WHEN("Layer height = nozzle_diameter[0]") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ mesh }, print, {
{ "support_material", 1 },
{ "layer_height", 0.2 },
{ "first_layer_height", 0.3 },
{ "dont_support_bridges", false },
});
bool a, b, c, d;
check(print, a, b, c, d);
THEN("First layer height is honored") { REQUIRE(a == true); }
THEN("No null or negative support layers") { REQUIRE(b == true); }
THEN("No layers thicker than nozzle diameter") { REQUIRE(c == true); }
// THEN("Layers above top surfaces are spaced correctly") { REQUIRE(d == true); }
}
}
}
#if 0
// Test 8.
TEST_CASE("SupportMaterial: forced support is generated", "[SupportMaterial]")
{
// Create a mesh & modelObject.
TriangleMesh mesh = TriangleMesh::make_cube(20, 20, 20);
Model model = Model();
ModelObject *object = model.add_object();
object->add_volume(mesh);
model.add_default_instances();
model.align_instances_to_origin();
Print print = Print();
std::vector<coordf_t> contact_z = {1.9};
std::vector<coordf_t> top_z = {1.1};
print.default_object_config.support_material_enforce_layers = 100;
print.default_object_config.support_material = 0;
print.default_object_config.layer_height = 0.2;
print.default_object_config.set_deserialize("first_layer_height", "0.3");
print.add_model_object(model.objects[0]);
print.objects.front()->_slice();
SupportMaterial *support = print.objects.front()->_support_material();
auto support_z = support->support_layers_z(contact_z, top_z, print.default_object_config.layer_height);
bool check = true;
for (size_t i = 1; i < support_z.size(); i++) {
if (support_z[i] - support_z[i - 1] <= 0)
check = false;
}
REQUIRE(check == true);
}
// TODO
bool test_6_checks(Print& print)
{
bool has_bridge_speed = true;
// Pre-Processing.
PrintObject* print_object = print.objects.front();
print_object->infill();
SupportMaterial* support_material = print.objects.front()->_support_material();
support_material->generate(print_object);
// TODO but not needed in test 6 (make brims and make skirts).
// Exporting gcode.
// TODO validation found in Simple.pm
return has_bridge_speed;
}
// Test 6.
SCENARIO("SupportMaterial: Checking bridge speed", "[SupportMaterial]")
{
GIVEN("Print object") {
// Create a mesh & modelObject.
TriangleMesh mesh = TriangleMesh::make_cube(20, 20, 20);
Model model = Model();
ModelObject *object = model.add_object();
object->add_volume(mesh);
model.add_default_instances();
model.align_instances_to_origin();
Print print = Print();
print.config.brim_width = 0;
print.config.skirts = 0;
print.config.skirts = 0;
print.default_object_config.support_material = 1;
print.default_region_config.top_solid_layers = 0; // so that we don't have the internal bridge over infill.
print.default_region_config.bridge_speed = 99;
print.config.cooling = 0;
print.config.set_deserialize("first_layer_speed", "100%");
WHEN("support_material_contact_distance = 0.2") {
print.default_object_config.support_material_contact_distance = 0.2;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0") {
print.default_object_config.support_material_contact_distance = 0;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
WHEN("support_material_contact_distance = 0.2 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0.2;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
}
}
#endif
/*
Old Perl tests, which were disabled by Vojtech at the time of first Support Generator refactoring.
#if 0
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('support_material', 1);
my @contact_z = my @top_z = ();
my $test = sub {
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $object_config = $print->print->objects->[0]->config;
my $flow = Slic3r::Flow->new_from_width(
width => $object_config->support_material_extrusion_width || $object_config->extrusion_width,
role => FLOW_ROLE_SUPPORT_MATERIAL,
nozzle_diameter => $print->config->nozzle_diameter->[$object_config->support_material_extruder-1] // $print->config->nozzle_diameter->[0],
layer_height => $object_config->layer_height,
);
my $support = Slic3r::Print::SupportMaterial->new(
object_config => $print->print->objects->[0]->config,
print_config => $print->print->config,
flow => $flow,
interface_flow => $flow,
first_layer_flow => $flow,
);
my $support_z = $support->support_layers_z($print->print->objects->[0], \@contact_z, \@top_z, $config->layer_height);
my $expected_top_spacing = $support->contact_distance($config->layer_height, $config->nozzle_diameter->[0]);
is $support_z->[0], $config->first_layer_height,
'first layer height is honored';
is scalar(grep { $support_z->[$_]-$support_z->[$_-1] <= 0 } 1..$#$support_z), 0,
'no null or negative support layers';
is scalar(grep { $support_z->[$_]-$support_z->[$_-1] > $config->nozzle_diameter->[0] + epsilon } 1..$#$support_z), 0,
'no layers thicker than nozzle diameter';
my $wrong_top_spacing = 0;
foreach my $top_z (@top_z) {
# find layer index of this top surface
my $layer_id = first { abs($support_z->[$_] - $top_z) < epsilon } 0..$#$support_z;
# check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
$wrong_top_spacing = 1
if ($support_z->[$layer_id+1] - $support_z->[$layer_id]) != $expected_top_spacing
&& ($support_z->[$layer_id+2] - $support_z->[$layer_id]) != $expected_top_spacing;
}
ok !$wrong_top_spacing, 'layers above top surfaces are spaced correctly';
};
$config->set('layer_height', 0.2);
$config->set('first_layer_height', 0.3);
@contact_z = (1.9);
@top_z = (1.1);
$test->();
$config->set('first_layer_height', 0.4);
$test->();
$config->set('layer_height', $config->nozzle_diameter->[0]);
$test->();
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('raft_layers', 3);
$config->set('brim_width', 0);
$config->set('skirts', 0);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
$config->set('layer_height', 0.4);
$config->set('first_layer_height', 0.4);
my $print = Slic3r::Test::init_print('overhang', config => $config);
ok my $gcode = Slic3r::Test::gcode($print), 'no conflict between raft/support and brim';
my $tool = 0;
Slic3r::GCode::Reader->new->parse($gcode, sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($info->{extruding}) {
if ($self->Z <= ($config->raft_layers * $config->layer_height)) {
fail 'not extruding raft with support material extruder'
if $tool != ($config->support_material_extruder-1);
} else {
fail 'support material exceeds raft layers'
if $tool == $config->support_material_extruder-1;
# TODO: we should test that full support is generated when we use raft too
}
}
});
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('raft_layers', 3);
$config->set('support_material_pattern', 'honeycomb');
$config->set('support_material_extrusion_width', 0.6);
$config->set('first_layer_extrusion_width', '100%');
$config->set('bridge_speed', 99);
$config->set('cooling', [ 0 ]); # prevent speed alteration
$config->set('first_layer_speed', '100%'); # prevent speed alteration
$config->set('start_gcode', ''); # prevent any unexpected Z move
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my $layer_id = -1; # so that first Z move sets this to 0
my @raft = my @first_object_layer = ();
my %first_object_layer_speeds = (); # F => 1
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($info->{extruding} && $info->{dist_XY} > 0) {
if ($layer_id <= $config->raft_layers) {
# this is a raft layer or the first object layer
my $line = Slic3r::Line->new_scale([ $self->X, $self->Y ], [ $info->{new_X}, $info->{new_Y} ]);
my @path = @{$line->grow(scale($config->support_material_extrusion_width/2))};
if ($layer_id < $config->raft_layers) {
# this is a raft layer
push @raft, @path;
} else {
push @first_object_layer, @path;
$first_object_layer_speeds{ $args->{F} // $self->F } = 1;
}
}
} elsif ($cmd eq 'G1' && $info->{dist_Z} > 0) {
$layer_id++;
}
});
ok !@{diff(\@first_object_layer, \@raft)},
'first object layer is completely supported by raft';
is scalar(keys %first_object_layer_speeds), 1,
'only one speed used in first object layer';
ok +(keys %first_object_layer_speeds)[0] == $config->bridge_speed*60,
'bridge speed used in first object layer';
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('layer_height', 0.35);
$config->set('first_layer_height', 0.3);
$config->set('nozzle_diameter', [0.5]);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
my $test = sub {
my ($raft_layers) = @_;
$config->set('raft_layers', $raft_layers);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
my %raft_z = (); # z => 1
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 ($info->{extruding} && $info->{dist_XY} > 0) {
if ($tool == $config->support_material_extruder-1) {
$raft_z{$self->Z} = 1;
}
}
});
is scalar(keys %raft_z), $config->raft_layers, 'correct number of raft layers is generated';
};
$test->(2);
$test->(70);
$config->set('layer_height', 0.4);
$config->set('first_layer_height', 0.35);
$test->(3);
$test->(70);
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('brim_width', 0);
$config->set('skirts', 0);
$config->set('support_material', 1);
$config->set('top_solid_layers', 0); # so that we don't have the internal bridge over infill
$config->set('bridge_speed', 99);
$config->set('cooling', [ 0 ]);
$config->set('first_layer_speed', '100%');
my $test = sub {
my $print = Slic3r::Test::init_print('overhang', config => $config);
my $has_bridge_speed = 0;
Slic3r::GCode::Reader->new->parse(Slic3r::Test::gcode($print), sub {
my ($self, $cmd, $args, $info) = @_;
if ($info->{extruding}) {
if (($args->{F} // $self->F) == $config->bridge_speed*60) {
$has_bridge_speed = 1;
}
}
});
return $has_bridge_speed;
};
$config->set('support_material_contact_distance', 0.2);
ok $test->(), 'bridge speed is used when support_material_contact_distance > 0';
$config->set('support_material_contact_distance', 0);
ok !$test->(), 'bridge speed is not used when support_material_contact_distance == 0';
$config->set('raft_layers', 5);
$config->set('support_material_contact_distance', 0.2);
ok $test->(), 'bridge speed is used when raft_layers > 0 and support_material_contact_distance > 0';
$config->set('support_material_contact_distance', 0);
ok !$test->(), 'bridge speed is not used when raft_layers > 0 and support_material_contact_distance == 0';
}
{
my $config = Slic3r::Config::new_from_defaults;
$config->set('skirts', 0);
$config->set('start_gcode', '');
$config->set('raft_layers', 8);
$config->set('nozzle_diameter', [0.4, 1]);
$config->set('layer_height', 0.1);
$config->set('first_layer_height', 0.8);
$config->set('support_material_extruder', 2);
$config->set('support_material_interface_extruder', 2);
$config->set('support_material_contact_distance', 0);
my $print = Slic3r::Test::init_print('20mm_cube', config => $config);
ok my $gcode = Slic3r::Test::gcode($print), 'first_layer_height is validated with support material extruder nozzle diameter when using raft layers';
my $tool = undef;
my @z = (0);
my %layer_heights_by_tool = (); # tool => [ lh, lh... ]
Slic3r::GCode::Reader->new->parse($gcode, sub {
my ($self, $cmd, $args, $info) = @_;
if ($cmd =~ /^T(\d+)/) {
$tool = $1;
} elsif ($cmd eq 'G1' && exists $args->{Z} && $args->{Z} != $self->Z) {
push @z, $args->{Z};
} elsif ($info->{extruding} && $info->{dist_XY} > 0) {
$layer_heights_by_tool{$tool} ||= [];
push @{ $layer_heights_by_tool{$tool} }, $z[-1] - $z[-2];
}
});
ok !defined(first { $_ > $config->nozzle_diameter->[0] + epsilon }
@{ $layer_heights_by_tool{$config->perimeter_extruder-1} }),
'no object layer is thicker than nozzle diameter';
ok !defined(first { abs($_ - $config->layer_height) < epsilon }
@{ $layer_heights_by_tool{$config->support_material_extruder-1} }),
'no support material layer is as thin as object layers';
}
*/

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>(0.5), scaled<double>(40.));
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>(0.5), scaled<double>(20.));
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>(0.5), scaled<double>(1.));
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>(0.25), scaled<double>(1.324888));
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>(0.5), scaled<double>(20.));
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>(0.5), scaled<double>(20.));
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(102499.75, 819998.);
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>(0.5), scaled<double>(4.));
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(500000, 1871238);
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

@@ -0,0 +1,359 @@
#include <catch2/catch.hpp>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleMeshSlicer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <future>
#include <chrono>
//#include "test_options.hpp"
#include "test_data.hpp"
using namespace Slic3r;
using namespace std;
static inline TriangleMesh make_cube() { return make_cube(20., 20, 20); }
SCENARIO( "TriangleMesh: Basic mesh statistics") {
GIVEN( "A 20mm cube, built from constexpr std::array" ) {
std::vector<Vec3f> vertices { {20,20,0}, {20,0,0}, {0,0,0}, {0,20,0}, {20,20,20}, {0,20,20}, {0,0,20}, {20,0,20} };
std::vector<Vec3i> facets { {0,1,2}, {0,2,3}, {4,5,6}, {4,6,7}, {0,4,7}, {0,7,1}, {1,7,6}, {1,6,2}, {2,6,5}, {2,5,3}, {4,0,3}, {4,3,5} };
TriangleMesh cube(vertices, facets);
THEN( "Volume is appropriate for 20mm square cube.") {
REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2);
}
THEN( "Vertices array matches input.") {
for (size_t i = 0U; i < cube.its.vertices.size(); i++) {
REQUIRE(cube.its.vertices.at(i) == vertices.at(i).cast<float>());
}
for (size_t i = 0U; i < vertices.size(); i++) {
REQUIRE(vertices.at(i).cast<float>() == cube.its.vertices.at(i));
}
}
THEN( "Vertex count matches vertex array size.") {
REQUIRE(cube.facets_count() == facets.size());
}
THEN( "Facet array matches input.") {
for (size_t i = 0U; i < cube.its.indices.size(); i++) {
REQUIRE(cube.its.indices.at(i) == facets.at(i));
}
for (size_t i = 0U; i < facets.size(); i++) {
REQUIRE(facets.at(i) == cube.its.indices.at(i));
}
}
THEN( "Facet count matches facet array size.") {
REQUIRE(cube.facets_count() == facets.size());
}
#if 0
THEN( "Number of normals is equal to the number of facets.") {
REQUIRE(cube.normals().size() == facets.size());
}
#endif
THEN( "center() returns the center of the object.") {
REQUIRE(cube.center() == Vec3d(10.0,10.0,10.0));
}
THEN( "Size of cube is (20,20,20)") {
REQUIRE(cube.size() == Vec3d(20,20,20));
}
}
}
SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "The cube is scaled 200% uniformly") {
cube.scale(2.0);
THEN( "The volume is equivalent to 40x40x40 (all dimensions increased by 200%") {
REQUIRE(abs(cube.volume() - 40.0*40.0*40.0) < 1e-2);
}
}
WHEN( "The resulting cube is scaled 200% in the X direction") {
cube.scale(Vec3f(2.0, 1, 1));
THEN( "The volume is doubled.") {
REQUIRE(abs(cube.volume() - 2*20.0*20.0*20.0) < 1e-2);
}
THEN( "The X coordinate size is 200%.") {
REQUIRE(cube.its.vertices.at(0).x() == 40.0);
}
}
WHEN( "The cube is scaled 25% in the X direction") {
cube.scale(Vec3f(0.25, 1, 1));
THEN( "The volume is 25% of the previous volume.") {
REQUIRE(abs(cube.volume() - 0.25*20.0*20.0*20.0) < 1e-2);
}
THEN( "The X coordinate size is 25% from previous.") {
REQUIRE(cube.its.vertices.at(0).x() == 5.0);
}
}
WHEN( "The cube is rotated 45 degrees.") {
cube.rotate_z(float(M_PI / 4.));
THEN( "The X component of the size is sqrt(2)*20") {
REQUIRE(abs(cube.size().x() - sqrt(2.0)*20) < 1e-2);
}
}
WHEN( "The cube is translated (5, 10, 0) units with a Vec3f ") {
cube.translate(Vec3f(5.0, 10.0, 0.0));
THEN( "The first vertex is located at 25, 30, 0") {
REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0));
}
}
WHEN( "The cube is translated (5, 10, 0) units with 3 doubles") {
cube.translate(5.0, 10.0, 0.0);
THEN( "The first vertex is located at 25, 30, 0") {
REQUIRE(cube.its.vertices.at(0) == Vec3f(25.0, 30.0, 0.0));
}
}
WHEN( "The cube is translated (5, 10, 0) units and then aligned to origin") {
cube.translate(5.0, 10.0, 0.0);
cube.align_to_origin();
THEN( "The third vertex is located at 0,0,0") {
REQUIRE(cube.its.vertices.at(2) == Vec3f::Zero());
}
THEN( "Size is OK") {
REQUIRE(cube.stats().size == Vec3f(20.f, 20.f, 20.f));
}
}
}
}
SCENARIO( "TriangleMesh: slice behavior.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN("Cube is sliced with z = [0+EPSILON,2,4,8,6,8,10,12,14,16,18,20]") {
std::vector<double> z { 0+EPSILON,2,4,8,6,8,10,12,14,16,18,20 };
std::vector<ExPolygons> result = cube.slice(z);
THEN( "The correct number of polygons are returned per layer.") {
for (size_t i = 0U; i < z.size(); i++) {
REQUIRE(result.at(i).size() == 1);
}
}
THEN( "The area of the returned polygons is correct.") {
for (size_t i = 0U; i < z.size(); i++) {
REQUIRE(result.at(i).at(0).area() == 20.0*20/(std::pow(SCALING_FACTOR,2)));
}
}
}
}
GIVEN( "A STL with an irregular shape.") {
const std::vector<Vec3f> vertices {{0,0,0},{0,0,20},{0,5,0},{0,5,20},{50,0,0},{50,0,20},{15,5,0},{35,5,0},{15,20,0},{50,5,0},{35,20,0},{15,5,10},{50,5,20},{35,5,10},{35,20,10},{15,20,10}};
const std::vector<Vec3i> facets {{0,1,2},{2,1,3},{1,0,4},{5,1,4},{0,2,4},{4,2,6},{7,6,8},{4,6,7},{9,4,7},{7,8,10},{2,3,6},{11,3,12},{7,12,9},{13,12,7},{6,3,11},{11,12,13},{3,1,5},{12,3,5},{5,4,9},{12,5,9},{13,7,10},{14,13,10},{8,15,10},{10,15,14},{6,11,8},{8,11,15},{15,11,13},{14,15,13}};
auto cube = make_cube();
WHEN(" a top tangent plane is sliced") {
// At Z = 10 we have a top horizontal surface.
std::vector<ExPolygons> slices = cube.slice({5.0, 10.0});
THEN( "its area is included") {
REQUIRE(slices.at(0).at(0).area() > 0);
REQUIRE(slices.at(1).at(0).area() > 0);
}
}
WHEN(" a model that has been transformed is sliced") {
cube.mirror_z();
std::vector<ExPolygons> slices = cube.slice({-5.0, -10.0});
THEN( "it is sliced properly (mirrored bottom plane area is included)") {
REQUIRE(slices.at(0).at(0).area() > 0);
REQUIRE(slices.at(1).at(0).area() > 0);
}
}
}
}
SCENARIO( "make_xxx functions produce meshes.") {
GIVEN("make_cube() function") {
WHEN("make_cube() is called with arguments 20,20,20") {
TriangleMesh cube = make_cube(20,20,20);
THEN("The resulting mesh has one and only one vertex at 0,0,0") {
const std::vector<Vec3f> &verts = cube.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1);
}
THEN("The mesh volume is 20*20*20") {
REQUIRE(abs(cube.volume() - 20.0*20.0*20.0) < 1e-2);
}
THEN("There are 12 facets.") {
REQUIRE(cube.its.indices.size() == 12);
}
}
}
GIVEN("make_cylinder() function") {
WHEN("make_cylinder() is called with arguments 10,10, PI / 3") {
TriangleMesh cyl = make_cylinder(10, 10, PI / 243.0);
double angle = (2*PI / floor(2*PI / (PI / 243.0)));
THEN("The resulting mesh has one and only one vertex at 0,0,0") {
const std::vector<Vec3f> &verts = cyl.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 0; } ) == 1);
}
THEN("The resulting mesh has one and only one vertex at 0,0,10") {
const std::vector<Vec3f> &verts = cyl.its.vertices;
REQUIRE(std::count_if(verts.begin(), verts.end(), [](const Vec3f& t) { return t.x() == 0 && t.y() == 0 && t.z() == 10; } ) == 1);
}
THEN("Resulting mesh has 2 + (2*PI/angle * 2) vertices.") {
REQUIRE(cyl.its.vertices.size() == (2 + ((2*PI/angle)*2)));
}
THEN("Resulting mesh has 2*PI/angle * 4 facets") {
REQUIRE(cyl.its.indices.size() == (2*PI/angle)*4);
}
THEN( "The mesh volume is approximately 10pi * 10^2") {
REQUIRE(abs(cyl.volume() - (10.0 * M_PI * std::pow(10,2))) < 1);
}
}
}
GIVEN("make_sphere() function") {
WHEN("make_sphere() is called with arguments 10, PI / 3") {
TriangleMesh sph = make_sphere(10, PI / 243.0);
THEN( "Edge length is smaller than limit but not smaller than half of it") {
double len = (sph.its.vertices[sph.its.indices[0][0]] - sph.its.vertices[sph.its.indices[0][1]]).norm();
double limit = 10*PI/243.;
REQUIRE(len <= limit);
REQUIRE(len >= limit/2.);
}
THEN( "Vertices are about the correct distance from the origin") {
bool all_vertices_ok = std::all_of(sph.its.vertices.begin(), sph.its.vertices.end(),
[](const stl_vertex& pt) { return is_approx(pt.squaredNorm(), 100.f); });
REQUIRE(all_vertices_ok);
}
THEN( "The mesh volume is approximately 4/3 * pi * 10^3") {
REQUIRE(abs(sph.volume() - (4.0/3.0 * M_PI * std::pow(10,3))) < 1); // 1% tolerance?
}
}
}
}
SCENARIO( "TriangleMesh: split functionality.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "The mesh is split into its component parts.") {
std::vector<TriangleMesh> meshes = cube.split();
THEN(" The bounding box statistics are propagated to the split copies") {
REQUIRE(meshes.size() == 1);
REQUIRE((meshes.front().bounding_box() == cube.bounding_box()));
}
}
}
GIVEN( "Two 20mm cubes, each with one corner on the origin, merged into a single TriangleMesh") {
auto cube = make_cube();
TriangleMesh cube2(cube);
cube.merge(cube2);
WHEN( "The combined mesh is split") {
THEN( "Number of faces is 2x the source.") {
REQUIRE(cube.facets_count() == 2 * cube2.facets_count());
}
std::vector<TriangleMesh> meshes = cube.split();
THEN( "Two meshes are in the output vector.") {
REQUIRE(meshes.size() == 2);
}
}
}
}
SCENARIO( "TriangleMesh: Mesh merge functions") {
GIVEN( "Two 20mm cubes, each with one corner on the origin") {
auto cube = make_cube();
TriangleMesh cube2(cube);
WHEN( "The two meshes are merged") {
cube.merge(cube2);
THEN( "There are twice as many facets in the merged mesh as the original.") {
REQUIRE(cube.facets_count() == 2 * cube2.facets_count());
}
}
}
}
SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube();
WHEN( "Object is cut at the bottom") {
indexed_triangle_set upper {};
indexed_triangle_set lower {};
cut_mesh(cube.its, 0, &upper, &lower);
THEN("Upper mesh has all facets except those belonging to the slicing plane.") {
REQUIRE(upper.indices.size() == 12);
}
THEN("Lower mesh has no facets.") {
REQUIRE(lower.indices.size() == 0);
}
}
WHEN( "Object is cut at the center") {
indexed_triangle_set upper {};
indexed_triangle_set lower {};
cut_mesh(cube.its, 10, &upper, &lower);
THEN("Upper mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") {
REQUIRE(upper.indices.size() == 2+12+6);
}
THEN("Lower mesh has 2 external horizontal facets, 3 facets on each side, and 6 facets on the triangulated side (2 + 12 + 6).") {
REQUIRE(lower.indices.size() == 2+12+6);
}
}
}
}
#ifdef TEST_PERFORMANCE
TEST_CASE("Regression test for issue #4486 - files take forever to slice") {
TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl");
config.set("layer_height", 500);
config.set("first_layer_height", 250);
config.set("nozzle_diameter", 500);
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({mesh}, print, model, config);
print.status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";};
std::future<void> fut = std::async([&print] () { print.process(); });
std::chrono::milliseconds span {120000};
bool timedout {false};
if(fut.wait_for(span) == std::future_status::timeout) {
timedout = true;
}
REQUIRE(timedout == false);
}
#endif // TEST_PERFORMANCE
#ifdef BUILD_PROFILE
TEST_CASE("Profile test for issue #4486 - files take forever to slice") {
TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl");
config.set("layer_height", 500);
config.set("first_layer_height", 250);
config.set("nozzle_diameter", 500);
config.set("fill_density", "5%");
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({mesh}, print, model, config);
print.status_cb = [] (int ln, const std::string& msg) { Slic3r::Log::info("Print") << ln << " " << msg << "\n";};
print.process();
REQUIRE(true);
}
#endif //BUILD_PROFILE