update test

This commit is contained in:
QIDI TECH
2024-11-09 14:59:54 +08:00
parent c9ec3da208
commit cb4f6b9da4
35 changed files with 1372 additions and 6272 deletions

View File

@@ -14,6 +14,13 @@ add_executable(${_TEST_NAME}_tests
test_gaps.cpp
test_gcode.cpp
test_gcode_travels.cpp
test_seam_perimeters.cpp
test_seam_shells.cpp
test_seam_geometry.cpp
test_seam_aligned.cpp
test_seam_rear.cpp
test_seam_random.cpp
benchmark_seams.cpp
test_gcodefindreplace.cpp
test_gcodewriter.cpp
test_cancel_object.cpp
@@ -33,6 +40,7 @@ add_executable(${_TEST_NAME}_tests
)
target_link_libraries(${_TEST_NAME}_tests test_common libslic3r)
set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
target_compile_definitions(${_TEST_NAME}_tests PUBLIC CATCH_CONFIG_ENABLE_BENCHMARKING)
if (WIN32)
qidislicer_copy_dlls(${_TEST_NAME}_tests)

View File

@@ -0,0 +1,138 @@
#include <catch2/catch.hpp>
#include "test_data.hpp"
#include "libslic3r/GCode/SeamGeometry.hpp"
#include "libslic3r/GCode/SeamAligned.hpp"
#include "libslic3r/GCode/SeamRear.hpp"
#include "libslic3r/GCode/SeamRandom.hpp"
TEST_CASE_METHOD(Slic3r::Test::SeamsFixture, "Seam benchmarks", "[Seams][.Benchmarks]") {
BENCHMARK_ADVANCED("Create extrusions benchy")(Catch::Benchmark::Chronometer meter) {
meter.measure([&] { return Slic3r::Seams::Geometry::get_extrusions(print_object->layers()); });
};
using namespace Slic3r::Seams;
BENCHMARK_ADVANCED("Create shells benchy")(Catch::Benchmark::Chronometer meter) {
std::vector<Perimeters::LayerPerimeters> inputs;
inputs.reserve(meter.runs());
std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() {
return Slic3r::Seams::Perimeters::create_perimeters(
projected, layer_infos, painting, params.perimeter
);
});
meter.measure([&](const int i) {
return Shells::create_shells(std::move(inputs[i]), params.max_distance);
});
};
BENCHMARK_ADVANCED("Get layer infos benchy")(Catch::Benchmark::Chronometer meter) {
meter.measure([&] {
return Perimeters::get_layer_infos(
print_object->layers(), params.perimeter.elephant_foot_compensation
);
});
};
BENCHMARK_ADVANCED("Create perimeters benchy")(Catch::Benchmark::Chronometer meter) {
meter.measure([&] {
return Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter);
});
};
BENCHMARK_ADVANCED("Generate aligned seam benchy")(Catch::Benchmark::Chronometer meter) {
std::vector<Shells::Shells<>> inputs;
inputs.reserve(meter.runs());
std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() {
Slic3r::Seams::Perimeters::LayerPerimeters perimeters{
Slic3r::Seams::Perimeters::create_perimeters(
projected, layer_infos, painting, params.perimeter
)};
return Shells::create_shells(
std::move(perimeters), params.max_distance
);
});
meter.measure([&](const int i) {
return Aligned::get_object_seams(
std::move(inputs[i]), visibility_calculator, params.aligned
);
});
};
BENCHMARK_ADVANCED("Visibility constructor")(Catch::Benchmark::Chronometer meter) {
using Visibility = Slic3r::ModelInfo::Visibility;
std::vector<Catch::Benchmark::storage_for<Visibility>> storage(meter.runs());
meter.measure([&](const int i) {
storage[i].construct(transformation, volumes, params.visibility, []() {});
});
};
BENCHMARK_ADVANCED("Generate rear seam benchy")(Catch::Benchmark::Chronometer meter) {
std::vector<Perimeters::LayerPerimeters> inputs;
inputs.reserve(meter.runs());
std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() {
return Slic3r::Seams::Perimeters::create_perimeters(
projected, layer_infos, painting, params.perimeter
);
});
meter.measure([&](const int i) {
return Rear::get_object_seams(std::move(inputs[i]), params.rear_tolerance, params.rear_y_offset);
});
};
BENCHMARK_ADVANCED("Generate random seam benchy")(Catch::Benchmark::Chronometer meter) {
std::vector<Perimeters::LayerPerimeters> inputs;
inputs.reserve(meter.runs());
std::generate_n(std::back_inserter(inputs), meter.runs(), [&]() {
return Slic3r::Seams::Perimeters::create_perimeters(
projected, layer_infos, painting, params.perimeter
);
});
meter.measure([&](const int i) {
return Random::get_object_seams(std::move(inputs[i]), params.random_seed);
});
};
Placer placer;
BENCHMARK_ADVANCED("Init seam placer aligned")(Catch::Benchmark::Chronometer meter) {
meter.measure([&] {
return placer.init(print->objects(), params, [](){});
});
};
SECTION("Place seam"){
using namespace Slic3r;
Placer placer;
placer.init(print->objects(), params, [](){});
std::vector<std::pair<const Layer*, const ExtrusionLoop*>> loops;
const PrintObject* object{print->objects().front()};
for (const Layer* layer :object->layers()) {
for (const LayerSlice& lslice : layer->lslices_ex) {
for (const LayerIsland &island : lslice.islands) {
const LayerRegion &layer_region = *layer->get_region(island.perimeters.region());
for (uint32_t perimeter_id : island.perimeters) {
const auto *entity_collection{static_cast<const ExtrusionEntityCollection*>(layer_region.perimeters().entities[perimeter_id])};
if (entity_collection != nullptr) {
for (const ExtrusionEntity *entity : *entity_collection) {
const auto loop{static_cast<const ExtrusionLoop*>(entity)};
if (loop == nullptr) {
continue;
}
loops.emplace_back(layer, loop);
}
}
}
}
}
}
BENCHMARK_ADVANCED("Place seam benchy")(Catch::Benchmark::Chronometer meter) {
meter.measure([&] {
for (const auto &[layer, loop] : loops) {
placer.place_seam(layer, *loop, {0, 0});
}
});
};
}
}

View File

@@ -251,8 +251,8 @@ SCENARIO("Cooling integration tests", "[Cooling]") {
l = line.dist_Z(self);
if (l > 0.) {
if (!layer_times.empty()) { // Ignore anything before first z move.
layer_times.back() += 60. * std::abs(l) / line.new_F(self);
}
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())];

File diff suppressed because one or more lines are too long

View File

@@ -2,12 +2,19 @@
#define SLIC3R_TEST_DATA_HPP
#include "libslic3r/Config.hpp"
#include "libslic3r/Format/3mf.hpp"
#include "libslic3r/GCode/ModelVisibility.hpp"
#include "libslic3r/GCode/SeamGeometry.hpp"
#include "libslic3r/GCode/SeamPerimeters.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/GCode/SeamPlacer.hpp"
#include "libslic3r/GCode/SeamAligned.hpp"
#include <filesystem>
#include <unordered_map>
namespace Slic3r { namespace Test {
@@ -39,14 +46,13 @@ enum class TestMesh {
};
// Neccessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
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;
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
@@ -56,35 +62,171 @@ 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) { return std::abs(a - b) < EPSILON; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return 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, unsigned duplicate_count = 1);
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, unsigned duplicate_count = 1);
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, unsigned duplicate = 1);
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, unsigned duplicate = 1);
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, unsigned duplicate = 1);
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,
unsigned duplicate_count = 1
);
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,
unsigned duplicate_count = 1
);
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,
unsigned duplicate = 1
);
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,
unsigned duplicate = 1
);
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,
unsigned duplicate = 1
);
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);
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 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);
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
inline std::unique_ptr<Print> process_3mf(const std::filesystem::path &path) {
DynamicPrintConfig config;
auto print{std::make_unique<Print>()};
Model model;
ConfigSubstitutionContext context{ForwardCompatibilitySubstitutionRule::Disable};
load_3mf(path.string().c_str(), config, context, &model, false);
Slic3r::Test::init_print(std::vector<TriangleMesh>{}, *print, model, config);
print->process();
return print;
}
static std::map<std::string, std::unique_ptr<Print>> prints_3mfs;
// Lazy getter, to avoid processing the 3mf multiple times, it already takes ages.
inline Print *get_print(const std::filesystem::path &file_path) {
if (!prints_3mfs.count(file_path.string())) {
prints_3mfs[file_path.string()] = process_3mf(file_path.string());
}
return prints_3mfs[file_path.string()].get();
}
inline void serialize_seam(std::ostream &output, const std::vector<std::vector<Seams::SeamPerimeterChoice>> &seam) {
output << "x,y,z,layer_index" << std::endl;
for (const std::vector<Seams::SeamPerimeterChoice> &layer : seam) {
if (layer.empty()) {
continue;
}
const Seams::SeamPerimeterChoice &choice{layer.front()};
// clang-format off
output
<< choice.choice.position.x() << ","
<< choice.choice.position.y() << ","
<< choice.perimeter.slice_z << ","
<< choice.perimeter.layer_index << std::endl;
// clang-format on
}
}
struct SeamsFixture
{
const std::filesystem::path file_3mf{
std::filesystem::path{TEST_DATA_DIR} / std::filesystem::path{"seam_test_object.3mf"}};
const Print *print{Test::get_print(file_3mf)};
const PrintObject *print_object{print->objects()[0]};
Seams::Params params{Seams::Placer::get_params(print->full_print_config())};
const Transform3d transformation{print_object->trafo_centered()};
const ModelVolumePtrs &volumes{print_object->model_object()->volumes};
Seams::ModelInfo::Painting painting{transformation, volumes};
const std::vector<Seams::Geometry::Extrusions> extrusions{
Seams::Geometry::get_extrusions(print_object->layers())};
const Seams::Perimeters::LayerInfos layer_infos{Seams::Perimeters::get_layer_infos(
print_object->layers(), params.perimeter.elephant_foot_compensation
)};
const std::vector<Seams::Geometry::BoundedPolygons> projected{
Seams::Geometry::project_to_geometry(extrusions, params.max_distance)};
const ModelInfo::Visibility visibility{transformation, volumes, params.visibility, [](){}};
Seams::Aligned::VisibilityCalculator
visibility_calculator{visibility, params.convex_visibility_modifier, params.concave_visibility_modifier};
};
}} // namespace Slic3r::Test
#endif // SLIC3R_TEST_DATA_HPP

View File

@@ -29,6 +29,7 @@ SCENARIO("Origin manipulation", "[GCode]") {
}
}
TEST_CASE("Wiping speeds", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
@@ -60,10 +61,11 @@ TEST_CASE("Wiping speeds", "[GCode]") {
for (const double retract_speed : retract_speeds) {
INFO("Wipe moves don\'t retract faster than configured speed");
CHECK(retract_speed < expected_retract_speed);
}
}
INFO("No wiping after layer change")
CHECK(!wiping_on_new_layer);
}
}
bool has_moves_below_z_offset(const DynamicPrintConfig& config) {
GCodeReader parser;
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
@@ -73,10 +75,11 @@ bool has_moves_below_z_offset(const DynamicPrintConfig& config) {
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.travel() && line.has_z() && line.z() < configured_offset) {
moves_below_z_offset++;
}
}
});
return moves_below_z_offset > 0;
}
}
TEST_CASE("Z moves with offset", "[GCode]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
@@ -103,7 +106,7 @@ std::optional<double> parse_axis(const std::string& line, const std::string& axi
return std::stod(matchedValue);
}
return std::nullopt;
}
}
/**
* This tests the following behavior:
@@ -138,10 +141,12 @@ TEST_CASE("Extrusion, travels, temeperatures", "[GCode]") {
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config, false, 2);
std::string gcode = Test::gcode(print);
if constexpr (debug_files) {
std::ofstream gcode_file{"sequential_print.gcode"};
gcode_file << gcode;
}
parser.parse_buffer(gcode, [&] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
INFO("Unexpected E argument");
CHECK(!line.has_e());
@@ -159,11 +164,11 @@ TEST_CASE("Extrusion, travels, temeperatures", "[GCode]") {
const std::optional<double> parsed_temperature = parse_axis(line.raw(), "S");
if (!parsed_temperature) {
FAIL("Failed to parse temperature!");
}
}
if (temps.empty() || temps.back() != parsed_temperature) {
temps.emplace_back(*parsed_temperature);
}
}
}
});
const unsigned layer_count = 20 / 0.4;
@@ -177,7 +182,7 @@ TEST_CASE("Extrusion, travels, temeperatures", "[GCode]") {
INFO("All travel moves happen within skirt.");
for (const Point& travel_move : travel_moves) {
CHECK(convex_hull.contains(travel_move));
}
}
INFO("Expected temperature changes");
CHECK(temps == std::vector<double>{210, 200, 210, 200, 0});
}
@@ -237,12 +242,13 @@ TEST_CASE("M73s have correct percent values", "[GCode]") {
config.set_deserialize_strict({
{" gcode_flavor", "sailfish" },
{" raft_layers", 3 },
});
});
Print print;
Model model;
Test::init_print({TestMesh::cube_20x20x20}, print, model, config);
check_m73s(print);
}
}
SECTION("Two copies of single object") {
config.set_deserialize_strict({
@@ -258,7 +264,7 @@ TEST_CASE("M73s have correct percent values", "[GCode]") {
std::ofstream gcode_file{"M73_2_copies.gcode"};
gcode_file << Test::gcode(print);
}
}
}
SECTION("Two objects") {
config.set_deserialize_strict({
@@ -307,8 +313,7 @@ TEST_CASE("M201 for acceleation reset", "[GCode]") {
if (line.cmd_is("M201") && line.has_x() && line.has_y()) {
if (line.x() == 1337 && line.y() == 1337) {
has_accel = true;
}
}
}
if (line.cmd_is("M204") && line.raw().find('S') != std::string::npos) {
has_m204 = true;

View File

@@ -6,7 +6,6 @@
using namespace Slic3r;
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
GIVEN("GCodeWriter instance") {

View File

@@ -71,10 +71,9 @@ SCENARIO("Perimeter nesting", "[Perimeters]")
nullptr,
nullptr,
// cache:
upper_layer_polygons_cache,
lower_layer_polygons_cache,
// output:
loops, gap_fill, fill_expolygons,fill_expolygons_no_overlap,0);
loops, gap_fill, fill_expolygons,fill_expolygons_no_overlap);
THEN("expected number of collections") {
REQUIRE(loops.entities.size() == data.expolygons.size());

View File

@@ -0,0 +1,164 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamAligned.hpp>
#include "test_data.hpp"
#include <fstream>
using namespace Slic3r;
using namespace Slic3r::Seams;
constexpr bool debug_files{false};
namespace AlignedTest {
Perimeters::Perimeter get_perimeter() {
const double slice_z{1.0};
const std::size_t layer_index{};
std::vector<Vec2d> positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.0, 1.0}, {0.0, 0.5}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
angles[4] = 0.0;
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> angle_type(positions.size(), Perimeters::AngleType::concave);
angle_type[4] = Perimeters::AngleType::smooth;
return {
slice_z,
layer_index,
false,
std::move(positions),
std::move(angles),
std::move(point_types),
std::move(point_classifications),
std::move(angle_type)};
}
} // namespace AlignedTest
TEST_CASE("Snap to angle", "[Seams][SeamAligned]") {
const Vec2d point{0.0, 0.4};
const std::size_t search_start{4};
const Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()};
std::optional<std::size_t> snapped_to{
Aligned::Impl::snap_to_angle(point, search_start, perimeter, 0.5)};
REQUIRE(snapped_to);
CHECK(*snapped_to == 0);
snapped_to = Aligned::Impl::snap_to_angle(point, search_start, perimeter, 0.3);
REQUIRE(!snapped_to);
}
TEST_CASE("Get seam options", "[Seams][SeamAligned]") {
Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()};
const Vec2d prefered_position{0.0, 0.3};
Aligned::Impl::SeamOptions options{Aligned::Impl::get_seam_options(
perimeter, prefered_position, *perimeter.common_points.common_points, 0.4
)};
CHECK(options.closest == 4);
CHECK(options.adjacent == 0);
CHECK((options.on_edge - Vec2d{0.0, 0.3}).norm() == Approx(0.0));
REQUIRE(options.snapped);
CHECK(options.snapped == 0);
}
struct PickSeamOptionFixture
{
Perimeters::Perimeter perimeter{AlignedTest::get_perimeter()};
Aligned::Impl::SeamOptions options{
4, // closest
0, // adjacent
true, // forward
false, // snapped
Vec2d{0.0, 0.3}, // on_edge
};
};
TEST_CASE_METHOD(PickSeamOptionFixture, "Pick seam option", "[Seams][SeamAligned]") {
auto [previous_index, next_index, position]{pick_seam_option(perimeter, options)};
CHECK(previous_index == next_index);
CHECK((position - Vec2d{0.0, 0.0}).norm() == Approx(0.0));
}
TEST_CASE_METHOD(PickSeamOptionFixture, "Pick seam option picks enforcer", "[Seams][SeamAligned]") {
perimeter.point_types[4] = Perimeters::PointType::enforcer;
auto [previous_index, next_index, position]{pick_seam_option(perimeter, options)};
CHECK(previous_index == next_index);
CHECK((position - Vec2d{0.0, 0.5}).norm() == Approx(0.0));
}
TEST_CASE_METHOD(PickSeamOptionFixture, "Nearest point", "[Seams][SeamAligned]") {
const std::optional<SeamChoice> result{Aligned::Impl::Nearest{Vec2d{0.4, -0.1}, 0.2}(
perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common
)};
CHECK(result->previous_index == 0);
CHECK(result->next_index == 1);
CHECK((result->position - Vec2d{0.4, 0.0}).norm() == Approx(0.0));
}
TEST_CASE_METHOD(PickSeamOptionFixture, "Least visible point", "[Seams][SeamAligned]") {
std::vector<double> precalculated_visibility{};
for (std::size_t i{0}; i < perimeter.positions.size(); ++i) {
precalculated_visibility.push_back(-static_cast<double>(i));
}
Aligned::Impl::LeastVisible least_visible{precalculated_visibility};
const std::optional<SeamChoice> result{least_visible(
perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common
)};
CHECK(result->previous_index == 4);
CHECK(result->next_index == 4);
CHECK((result->position - Vec2d{0.0, 0.5}).norm() == Approx(0.0));
}
TEST_CASE_METHOD(Test::SeamsFixture, "Generate aligned seam", "[Seams][SeamAligned][Integration]") {
Seams::Perimeters::LayerPerimeters perimeters{
Seams::Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter)};
Seams::Shells::Shells<> shells{
Seams::Shells::create_shells(std::move(perimeters), params.max_distance)};
const std::vector<std::vector<SeamPerimeterChoice>> seam{
Aligned::get_object_seams(std::move(shells), visibility_calculator, params.aligned)};
if constexpr (debug_files) {
std::ofstream csv{"aligned_seam.csv"};
Test::serialize_seam(csv, seam);
}
}
TEST_CASE_METHOD(Test::SeamsFixture, "Calculate visibility", "[Seams][SeamAligned][Integration]") {
if constexpr (debug_files) {
std::ofstream csv{"visibility.csv"};
csv << "x,y,z,visibility,total_visibility" << std::endl;
Seams::Perimeters::LayerPerimeters perimeters{
Seams::Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter)};
Seams::Shells::Shells<> shells{
Seams::Shells::create_shells(std::move(perimeters), params.max_distance)};
for (const Shells::Shell<> &shell : shells) {
for (const Shells::Slice<> &slice : shell) {
for (std::size_t index{0}; index < slice.boundary.positions.size(); ++index) {
const Vec2d &position{slice.boundary.positions[index]};
const double point_visibility{visibility.calculate_point_visibility(
to_3d(position.cast<float>(), slice.boundary.slice_z)
)};
const double total_visibility{
visibility_calculator(SeamChoice{index, index, position}, slice.boundary)};
// clang-format off
csv <<
position.x() << "," <<
position.y() << "," <<
slice.boundary.slice_z << "," <<
point_visibility << "," <<
total_visibility << std::endl;
// clang-format on
}
}
}
}
}

View File

@@ -0,0 +1,145 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamGeometry.hpp>
#include <libslic3r/Geometry.hpp>
using namespace Slic3r;
TEST_CASE("Lists mapping", "[Seams][SeamGeometry]") {
// clang-format off
std::vector<std::vector<int>> list_of_lists{
{},
{7, 2, 3},
{9, 6, 3, 6, 7},
{1, 1, 3},
{1},
{3},
{1},
{},
{3}
};
// clang-format on
std::vector<std::size_t> sizes;
sizes.reserve(list_of_lists.size());
for (const std::vector<int> &list : list_of_lists) {
sizes.push_back(list.size());
}
const auto [mapping, bucket_cout]{Seams::Geometry::get_mapping(
sizes,
[&](const std::size_t layer_index,
const std::size_t item_index) -> Seams::Geometry::MappingOperatorResult {
unsigned max_diff{0};
std::optional<std::size_t> index;
const std::vector<int> &layer{list_of_lists[layer_index]};
const std::vector<int> &next_layer{list_of_lists[layer_index + 1]};
for (std::size_t i{0}; i < next_layer.size(); ++i) {
const long diff{std::abs(next_layer[i] - layer[item_index])};
if (diff > max_diff) {
max_diff = diff;
index = i;
}
}
if (!index) {
return std::nullopt;
}
return std::pair{*index, static_cast<double>(max_diff)};
}
)};
// clang-format off
CHECK(mapping == std::vector<std::vector<std::size_t>>{
{},
{0, 1, 2},
{1, 3, 0, 4, 5},
{1, 6, 7},
{7},
{7},
{7},
{},
{8}
});
// clang-format on
}
TEST_CASE("Vertex angle calculation counterclockwise", "[Seams][SeamGeometry]") {
std::vector<Vec2d> points{Vec2d{0, 0}, Vec2d{1, 0}, Vec2d{1, 1}, Vec2d{0, 1}};
std::vector<double> angles{Seams::Geometry::get_vertex_angles(points, 0.1)};
CHECK(angles.size() == 4);
for (const double angle : angles) {
CHECK(angle == Approx(-M_PI / 2));
}
}
TEST_CASE("Vertex angle calculation clockwise", "[Seams][SeamGeometry]") {
std::vector<Vec2d> points = {Vec2d{0, 0}, Vec2d{0, 1}, Vec2d{1, 1}, Vec2d{1, 0}};
std::vector<double> angles = Seams::Geometry::get_vertex_angles(points, 0.1);
CHECK(angles.size() == 4);
for (const double angle : angles) {
CHECK(angle == Approx(M_PI / 2));
}
}
TEST_CASE("Vertex angle calculation small convex", "[Seams][SeamGeometry]") {
std::vector<Vec2d> points = {Vec2d{0, 0}, Vec2d{-0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> angles = Seams::Geometry::get_vertex_angles(points, 0.1);
CHECK(angles.size() == 4);
CHECK(angles[1] > 0);
CHECK(angles[1] < 0.02);
}
TEST_CASE("Vertex angle calculation small concave", "[Seams][SeamGeometry]") {
std::vector<Vec2d> points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> angles = Seams::Geometry::get_vertex_angles(points, 0.1);
CHECK(angles.size() == 4);
CHECK(angles[1] < 0);
CHECK(angles[1] > -0.02);
}
TEST_CASE("Vertex angle is rotation agnostic", "[Seams][SeamGeometry]") {
std::vector<Vec2d> points = {Vec2d{0, 0}, Vec2d{0.01, 1}, Vec2d{0, 2}, Vec2d{-2, 1}};
std::vector<double> angles = Seams::Geometry::get_vertex_angles(points, 0.1);
Points polygon_points;
using std::transform, std::back_inserter;
transform(points.begin(), points.end(), back_inserter(polygon_points), [](const Vec2d &point) {
return scaled(point);
});
Polygon polygon{polygon_points};
polygon.rotate(M_PI - Slic3r::Geometry::deg2rad(10.0));
std::vector<Vec2d> rotated_points;
using std::transform, std::back_inserter;
transform(
polygon.points.begin(), polygon.points.end(), back_inserter(rotated_points),
[](const Point &point) { return unscaled(point); }
);
std::vector<double> rotated_angles = Seams::Geometry::get_vertex_angles(points, 0.1);
CHECK(rotated_angles[1] == Approx(angles[1]));
}
TEST_CASE("Calculate overhangs", "[Seams][SeamGeometry]") {
const ExPolygon square{
scaled(Vec2d{0.0, 0.0}),
scaled(Vec2d{1.0, 0.0}),
scaled(Vec2d{1.0, 1.0}),
scaled(Vec2d{0.0, 1.0})
};
const std::vector<Vec2d> points{Seams::Geometry::unscaled(square.contour.points)};
ExPolygon previous_layer{square};
previous_layer.translate(scaled(Vec2d{-0.5, 0}));
AABBTreeLines::LinesDistancer<Linef> previous_layer_distancer{
to_unscaled_linesf({previous_layer})};
const std::vector<double> overhangs{
Seams::Geometry::get_overhangs(points, previous_layer_distancer, 0.5)};
REQUIRE(overhangs.size() == points.size());
CHECK_THAT(overhangs, Catch::Matchers::Approx(std::vector<double>{
0.0, M_PI / 4.0, M_PI / 4.0, 0.0
}));
}

View File

@@ -0,0 +1,180 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/SeamPerimeters.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamGeometry.hpp>
#include <libslic3r/Geometry.hpp>
#include <fstream>
#include "test_data.hpp"
using namespace Slic3r;
using namespace Slic3r::Seams;
constexpr bool debug_files{false};
const ExPolygon square{
scaled(Vec2d{0.0, 0.0}), scaled(Vec2d{1.0, 0.0}), scaled(Vec2d{1.0, 1.0}),
scaled(Vec2d{0.0, 1.0})};
TEST_CASE("Oversample painted", "[Seams][SeamPerimeters]") {
auto is_painted{[](const Vec3f &position, float radius) {
return (position - Vec3f{0.5, 0.0, 1.0}).norm() < radius;
}};
std::vector<Vec2d> points{Perimeters::Impl::oversample_painted(
Seams::Geometry::unscaled(square.contour.points), is_painted, 1.0, 0.2
)};
REQUIRE(points.size() == 8);
CHECK((points[1] - Vec2d{0.2, 0.0}).norm() == Approx(0.0));
points = Perimeters::Impl::oversample_painted(
Seams::Geometry::unscaled(square.contour.points), is_painted, 1.0, 0.199
);
CHECK(points.size() == 9);
}
TEST_CASE("Remove redundant points", "[Seams][SeamPerimeters]") {
using Perimeters::PointType;
using Perimeters::PointClassification;
std::vector<Vec2d> points{{0.0, 0.0}, {1.0, 0.0}, {2.0, 0.0}, {3.0, 0.0},
{3.0, 1.0}, {3.0, 2.0}, {0.0, 2.0}};
std::vector<PointType> point_types{PointType::common,
PointType::enforcer, // Should keep this.
PointType::enforcer, // Should keep this.
PointType::blocker,
PointType::blocker, // Should remove this.
PointType::blocker, PointType::common};
const auto [resulting_points, resulting_point_types]{
Perimeters::Impl::remove_redundant_points(points, point_types, 0.1)};
REQUIRE(resulting_points.size() == 6);
REQUIRE(resulting_point_types.size() == 6);
CHECK((resulting_points[3] - Vec2d{3.0, 0.0}).norm() == Approx(0.0));
CHECK((resulting_points[4] - Vec2d{3.0, 2.0}).norm() == Approx(0.0));
CHECK(resulting_point_types[3] == PointType::blocker);
CHECK(resulting_point_types[4] == PointType::blocker);
}
TEST_CASE("Perimeter constructs KD trees", "[Seams][SeamPerimeters]") {
using Perimeters::PointType;
using Perimeters::PointClassification;
using Perimeters::AngleType;
std::vector<Vec2d> positions{Vec2d{0.0, 0.0}, Vec2d{1.0, 0.0}, Vec2d{1.0, 1.0}, Vec2d{0.0, 1.0}};
std::vector<double> angles(4, -M_PI / 2.0);
std::vector<PointType>
point_types{PointType::enforcer, PointType::blocker, PointType::common, PointType::common};
std::vector<PointClassification> point_classifications{
PointClassification::overhang, PointClassification::embedded, PointClassification::embedded,
PointClassification::common};
std::vector<AngleType>
angle_types{AngleType::convex, AngleType::concave, AngleType::smooth, AngleType::smooth};
Perimeters::Perimeter perimeter{
3.0,
2,
false,
std::move(positions),
std::move(angles),
std::move(point_types),
std::move(point_classifications),
std::move(angle_types)};
CHECK(perimeter.enforced_points.overhanging_points);
CHECK(perimeter.blocked_points.embedded_points);
CHECK(perimeter.common_points.common_points);
CHECK(perimeter.common_points.embedded_points);
}
using std::filesystem::path;
constexpr const char *to_string(Perimeters::PointType point_type) {
using Perimeters::PointType;
switch (point_type) {
case PointType::enforcer: return "enforcer";
case PointType::blocker: return "blocker";
case PointType::common: return "common";
}
throw std::runtime_error("Unreachable");
}
constexpr const char *to_string(Perimeters::PointClassification point_classification) {
using Perimeters::PointClassification;
switch (point_classification) {
case PointClassification::embedded: return "embedded";
case PointClassification::overhang: return "overhang";
case PointClassification::common: return "common";
}
throw std::runtime_error("Unreachable");
}
constexpr const char *to_string(Perimeters::AngleType angle_type) {
using Perimeters::AngleType;
switch (angle_type) {
case AngleType::convex: return "convex";
case AngleType::concave: return "concave";
case AngleType::smooth: return "smooth";
}
throw std::runtime_error("Unreachable");
}
void serialize_shell(std::ostream &output, const Shells::Shell<Perimeters::Perimeter> &shell) {
output << "x,y,z,point_type,point_classification,angle_type,layer_index,"
"point_index,distance,distance_to_previous,is_degenerate"
<< std::endl;
for (std::size_t perimeter_index{0}; perimeter_index < shell.size(); ++perimeter_index) {
const Shells::Slice<> &slice{shell[perimeter_index]};
const Perimeters::Perimeter &perimeter{slice.boundary};
const std::vector<Vec2d> &points{perimeter.positions};
double total_distance{0.0};
for (std::size_t point_index{0}; point_index < perimeter.point_types.size(); ++point_index) {
const Vec3d point{to_3d(points[point_index], perimeter.slice_z)};
const Perimeters::PointType point_type{perimeter.point_types[point_index]};
const Perimeters::PointClassification point_classification{
perimeter.point_classifications[point_index]};
const Perimeters::AngleType angle_type{perimeter.angle_types[point_index]};
const std::size_t layer_index{slice.layer_index};
const std::size_t previous_index{point_index == 0 ? points.size() - 1 : point_index - 1};
const double distance_to_previous{(points[point_index] - points[previous_index]).norm()};
total_distance += point_index == 0 ? 0.0 : distance_to_previous;
const double distance{total_distance};
const bool is_degenerate{perimeter.is_degenerate};
// clang-format off
output
<< point.x() << ","
<< point.y() << ","
<< point.z() << ","
<< to_string(point_type) << ","
<< to_string(point_classification) << ","
<< to_string(angle_type) << ","
<< layer_index << ","
<< point_index << ","
<< distance << ","
<< distance_to_previous << ","
<< is_degenerate << std::endl;
// clang-format on
}
}
}
TEST_CASE_METHOD(Test::SeamsFixture, "Create perimeters", "[Seams][SeamPerimeters][Integration]") {
Seams::Perimeters::LayerPerimeters perimeters{
Seams::Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter)};
Seams::Shells::Shells<> shells{
Seams::Shells::create_shells(std::move(perimeters), params.max_distance)};
if constexpr (debug_files) {
std::ofstream csv{"perimeters.csv"};
serialize_shell(csv, shells[0]);
}
}

View File

@@ -0,0 +1,96 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamRandom.hpp>
#include "test_data.hpp"
#include <fstream>
using namespace Slic3r;
using namespace Slic3r::Seams;
constexpr bool debug_files{false};
namespace RandomTest {
Perimeters::Perimeter get_perimeter() {
const double slice_z{1.0};
const std::size_t layer_index{};
std::vector<Vec2d> positions{{0.0, 0.0}, {0.5, 0.0}, {1.0, 0.0}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> angle_type(positions.size(), Perimeters::AngleType::concave);
return {
slice_z,
layer_index,
false,
std::move(positions),
std::move(angles),
std::move(point_types),
std::move(point_classifications),
std::move(angle_type)};
}
} // namespace RandomTest
double get_chi2_uniform(const std::vector<double> &data, double min, double max, const std::size_t bin_count) {
std::vector<std::size_t> bins(bin_count);
const double bin_size{(max - min) / bin_count};
const double expected_frequncy{static_cast<double>(data.size()) / bin_count};
for (const double value : data) {
auto bin{static_cast<int>(std::floor((value - min) / bin_size))};
bins[bin]++;
}
return std::accumulate(bins.begin(), bins.end(), 0.0, [&](const double total, const std::size_t count_in_bin){
return total + std::pow(static_cast<double>(count_in_bin - expected_frequncy), 2.0) / expected_frequncy;
});
}
TEST_CASE("Random is uniform", "[Seams][SeamRandom]") {
const int seed{42};
std::mt19937 random_engine{seed};
const Random::Impl::Random random{random_engine};
Perimeters::Perimeter perimeter{RandomTest::get_perimeter()};
std::vector<double> x_positions;
const std::size_t count{1001};
x_positions.reserve(count);
std::generate_n(std::back_inserter(x_positions), count, [&]() {
std::optional<SeamChoice> choice{
random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common)};
return choice->position.x();
});
const std::size_t degrees_of_freedom{10};
const double critical{18.307}; // dof 10, significance 0.05
CHECK(get_chi2_uniform(x_positions, 0.0, 1.0, degrees_of_freedom + 1) < critical);
}
TEST_CASE("Random respects point type", "[Seams][SeamRandom]") {
const int seed{42};
std::mt19937 random_engine{seed};
const Random::Impl::Random random{random_engine};
Perimeters::Perimeter perimeter{RandomTest::get_perimeter()};
std::optional<SeamChoice> choice{
random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common)};
REQUIRE(choice);
const std::size_t picked_index{choice->previous_index};
perimeter.point_types[picked_index] = Perimeters::PointType::blocker;
choice = random(perimeter, Perimeters::PointType::common, Perimeters::PointClassification::common);
REQUIRE(choice);
CHECK(choice->previous_index != picked_index);
}
TEST_CASE_METHOD(Test::SeamsFixture, "Generate random seam", "[Seams][SeamRandom][Integration]") {
Seams::Perimeters::LayerPerimeters perimeters{
Seams::Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter)};
const std::vector<std::vector<SeamPerimeterChoice>> seams{
Random::get_object_seams(std::move(perimeters), params.random_seed)};
if constexpr (debug_files) {
std::ofstream csv{"random_seam.csv"};
Test::serialize_seam(csv, seams);
}
}

View File

@@ -0,0 +1,47 @@
#include <libslic3r/Point.hpp>
#include <catch2/catch.hpp>
#include <libslic3r/GCode/SeamRear.hpp>
#include "test_data.hpp"
#include <fstream>
using namespace Slic3r;
using namespace Slic3r::Seams;
constexpr bool debug_files{false};
namespace RearTest {
Perimeters::Perimeter get_perimeter() {
const double slice_z{1.0};
const std::size_t layer_index{};
std::vector<Vec2d> positions{{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}, {0.5, 1.0}, {0.0, 1.0}};
std::vector<double> angles(positions.size(), -M_PI / 2.0);
angles[3] = 0.0;
std::vector<Perimeters::PointType> point_types(positions.size(), Perimeters::PointType::common);
std::vector<Perimeters::PointClassification>
point_classifications{positions.size(), Perimeters::PointClassification::common};
std::vector<Perimeters::AngleType> angle_type(positions.size(), Perimeters::AngleType::concave);
angle_type[3] = Perimeters::AngleType::smooth;
return {
slice_z,
layer_index,
false,
std::move(positions),
std::move(angles),
std::move(point_types),
std::move(point_classifications),
std::move(angle_type)};
}
} // namespace RearTest
TEST_CASE_METHOD(Test::SeamsFixture, "Generate rear seam", "[Seams][SeamRear][Integration]") {
Seams::Perimeters::LayerPerimeters perimeters{
Seams::Perimeters::create_perimeters(projected, layer_infos, painting, params.perimeter)};
const std::vector<std::vector<SeamPerimeterChoice>> seams{
Rear::get_object_seams(std::move(perimeters), params.rear_tolerance, params.rear_y_offset)};
if constexpr (debug_files) {
std::ofstream csv{"rear_seam.csv"};
Test::serialize_seam(csv, seams);
}
}

View File

@@ -0,0 +1,59 @@
#include <catch2/catch.hpp>
#include <filesystem>
#include <fstream>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/GCode/SeamPainting.hpp"
#include "test_data.hpp"
#include "libslic3r/GCode/SeamShells.hpp"
using namespace Slic3r;
using namespace Slic3r::Seams;
constexpr bool debug_files{false};
struct ProjectionFixture
{
Polygon extrusion_path{
Point{scaled(Vec2d{-1.0, -1.0})}, Point{scaled(Vec2d{1.0, -1.0})},
Point{scaled(Vec2d{1.0, 1.0})}, Point{scaled(Vec2d{-1.0, 1.0})}};
ExPolygon island_boundary;
Seams::Geometry::Extrusions extrusions;
double extrusion_width{0.2};
ProjectionFixture() {
extrusions.emplace_back(Polygon{extrusion_path}, extrusion_path.bounding_box(), extrusion_width, island_boundary);
}
};
TEST_CASE_METHOD(ProjectionFixture, "Project to geometry matches", "[Seams][SeamShells]") {
Polygon boundary_polygon{extrusion_path};
// Add + 0.1 to check that boundary polygon has been picked.
boundary_polygon.scale(1.0 + extrusion_width / 2.0 + 0.1);
island_boundary.contour = boundary_polygon;
Seams::Geometry::BoundedPolygons result{Seams::Geometry::project_to_geometry(extrusions, 5.0)};
REQUIRE(result.size() == 1);
REQUIRE(result[0].polygon.size() == 4);
// Boundary polygon is picked.
CHECK(result[0].polygon[0].x() == Approx(scaled(-(1.0 + extrusion_width / 2.0 + 0.1))));
}
TEST_CASE_METHOD(ProjectionFixture, "Project to geometry does not match", "[Seams][SeamShells]") {
Polygon boundary_polygon{extrusion_path};
// Island boundary is far from the extrusion.
boundary_polygon.scale(5.0);
island_boundary.contour = boundary_polygon;
Seams::Geometry::BoundedPolygons result{Seams::Geometry::project_to_geometry(extrusions, 1.0)};
REQUIRE(result.size() == 1);
REQUIRE(result[0].polygon.size() == 4);
const Polygon expanded{expand(extrusions.front().polygon, scaled(extrusion_width / 2.0)).front()};
// The extrusion is expanded and returned.
CHECK(result[0].polygon == expanded);
}