mirror of
https://github.com/QIDITECH/QIDISlicer.git
synced 2026-01-30 23:48:44 +03:00
QIDISlicer1.0.0
This commit is contained in:
38
tests/fff_print/CMakeLists.txt
Normal file
38
tests/fff_print/CMakeLists.txt
Normal 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})
|
||||
18
tests/fff_print/fff_print_tests.cpp
Normal file
18
tests/fff_print/fff_print_tests.cpp
Normal 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
|
||||
16
tests/fff_print/test_avoid_crossing_perimeters.cpp
Normal file
16
tests/fff_print/test_avoid_crossing_perimeters.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
133
tests/fff_print/test_bridges.cpp
Normal file
133
tests/fff_print/test_bridges.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
203
tests/fff_print/test_clipper.cpp
Normal file
203
tests/fff_print/test_clipper.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
275
tests/fff_print/test_cooling.cpp
Normal file
275
tests/fff_print/test_cooling.cpp
Normal 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, '';
|
||||
}
|
||||
}
|
||||
}
|
||||
273
tests/fff_print/test_custom_gcode.cpp
Normal file
273
tests/fff_print/test_custom_gcode.cpp
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
401
tests/fff_print/test_data.cpp
Normal file
401
tests/fff_print/test_data.cpp
Normal file
File diff suppressed because one or more lines are too long
90
tests/fff_print/test_data.hpp
Normal file
90
tests/fff_print/test_data.hpp
Normal 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
|
||||
409
tests/fff_print/test_extrusion_entity.cpp
Normal file
409
tests/fff_print/test_extrusion_entity.cpp
Normal 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);
|
||||
}
|
||||
715
tests/fff_print/test_fill.cpp
Normal file
715
tests/fff_print/test_fill.cpp
Normal 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
|
||||
}
|
||||
226
tests/fff_print/test_flow.cpp
Normal file
226
tests/fff_print/test_flow.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
tests/fff_print/test_gaps.cpp
Normal file
60
tests/fff_print/test_gaps.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
tests/fff_print/test_gcode.cpp
Normal file
22
tests/fff_print/test_gcode.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
292
tests/fff_print/test_gcodefindreplace.cpp
Normal file
292
tests/fff_print/test_gcodefindreplace.cpp
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
96
tests/fff_print/test_gcodewriter.cpp
Normal file
96
tests/fff_print/test_gcodewriter.cpp
Normal 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
tests/fff_print/test_model.cpp
Normal file
61
tests/fff_print/test_model.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
271
tests/fff_print/test_multi.cpp
Normal file
271
tests/fff_print/test_multi.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
629
tests/fff_print/test_perimeters.cpp
Normal file
629
tests/fff_print/test_perimeters.cpp
Normal 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, ¤t_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, ¤t_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);
|
||||
}
|
||||
}
|
||||
187
tests/fff_print/test_print.cpp
Normal file
187
tests/fff_print/test_print.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
271
tests/fff_print/test_printgcode.cpp
Normal file
271
tests/fff_print/test_printgcode.cpp
Normal 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.));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
tests/fff_print/test_printobject.cpp
Normal file
89
tests/fff_print/test_printobject.cpp
Normal 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
|
||||
}
|
||||
}
|
||||
405
tests/fff_print/test_shells.cpp
Normal file
405
tests/fff_print/test_shells.cpp
Normal 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
|
||||
268
tests/fff_print/test_skirt_brim.cpp
Normal file
268
tests/fff_print/test_skirt_brim.cpp
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
497
tests/fff_print/test_support_material.cpp
Normal file
497
tests/fff_print/test_support_material.cpp
Normal 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';
|
||||
}
|
||||
|
||||
*/
|
||||
191
tests/fff_print/test_thin_walls.cpp
Normal file
191
tests/fff_print/test_thin_walls.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
359
tests/fff_print/test_trianglemesh.cpp
Normal file
359
tests/fff_print/test_trianglemesh.cpp
Normal 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
|
||||
Reference in New Issue
Block a user