From 5fafbb59fca10c2c44781e8173671e6b61a2e890 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Sun, 14 Jun 2026 04:42:53 -0500 Subject: [PATCH] Revive the disabled fff_print test suite (#14196) * Fix null-deref and arranger bugs that gate headless slicing tests export_gcode dereferenced a null result out-param, enum serialization dereferenced a null keys_map, and get_arrange_polys left bed_idx unseeded so the arranger dropped items. All only affect the headless test/CLI path. * Fix the headless test harness and add G-code test helpers Use the real arranger, fix temp-file handling with an RAII guard, and add layers_with_role / max_z for inspecting sliced G-code. * Re-enable the Model construction test * Re-enable SupportMaterial tests and add an enforced-support test * Re-enable and extend PrintObject layer-height and perimeter tests * Re-enable Print skirt, brim, and solid-surface tests * Re-enable and extend PrintGCode tests Un-hide the basic scenario (dead-key fixes, reframes, trimmed trivia) and add initial-layer-height, sequential-order, and null-result export tests. * Re-enable and reframe the skirt/brim tests Detect skirt/brim by G-code role comment instead of a sentinel speed, and resolve the previously-unfinished skirt-enclosure test. * Replace the stale lift()/unlift() test with a z_hop test * Delete the stub and broken Flow tests --- src/libslic3r/Config.hpp | 2 +- src/libslic3r/GCode.cpp | 2 +- src/libslic3r/ModelArrange.cpp | 4 + src/libslic3r/Print.cpp | 3 +- .../test_gcodewriter/config_lift_unlift.ini | 30 --- tests/fff_print/test_data.cpp | 29 ++- tests/fff_print/test_data.hpp | 9 + tests/fff_print/test_flow.cpp | 77 +----- tests/fff_print/test_gcodewriter.cpp | 87 ++----- tests/fff_print/test_model.cpp | 13 +- tests/fff_print/test_print.cpp | 79 ++---- tests/fff_print/test_printgcode.cpp | 210 +++++++++------- tests/fff_print/test_printobject.cpp | 91 ++++--- tests/fff_print/test_skirt_brim.cpp | 206 +++++++--------- tests/fff_print/test_support_material.cpp | 229 ++++-------------- tests/test_utils.hpp | 24 ++ 16 files changed, 419 insertions(+), 676 deletions(-) delete mode 100644 tests/data/fff_print_tests/test_gcodewriter/config_lift_unlift.ini diff --git a/src/libslic3r/Config.hpp b/src/libslic3r/Config.hpp index 8ef014564c..5fedaa9b28 100644 --- a/src/libslic3r/Config.hpp +++ b/src/libslic3r/Config.hpp @@ -2186,7 +2186,7 @@ private: else throw ConfigurationError("Serializing NaN"); } - else { + else if (this->keys_map != nullptr) { for (const auto& kvp : *this->keys_map) if (kvp.second == v) ss << kvp.first; diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 54b4d4bcff..cdcca01f38 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2194,7 +2194,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu BOOST_LOG_TRIVIAL(info) << "Exporting G-code finished" << log_memory_info(); print->set_done(psGCodeExport); - if(is_BBL_Printer()) + if(is_BBL_Printer() && result != nullptr) result->label_object_enabled = m_enable_exclude_object; // Write the profiler measurements to file PROFILE_UPDATE(); diff --git a/src/libslic3r/ModelArrange.cpp b/src/libslic3r/ModelArrange.cpp index 477509e69e..95167565aa 100644 --- a/src/libslic3r/ModelArrange.cpp +++ b/src/libslic3r/ModelArrange.cpp @@ -19,6 +19,10 @@ arrangement::ArrangePolygons get_arrange_polys(const Model &model, ModelInstance for (ModelObject *mo : model.objects) for (ModelInstance *minst : mo->instances) { minst->get_arrange_polygon(&ap); + // ModelInstance::get_arrange_polygon leaves bed_idx at its UNARRANGED + // default; seed it to bed 0 (as get_instance_arrange_poly does) so the + // nester treats the item as placeable instead of returning it unplaced. + ap.bed_idx = 0; input.emplace_back(ap); instances.emplace_back(minst); } diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 0d58e2dc00..78bdc8f1cf 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -2632,7 +2632,8 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor gcode.do_export(this, path.c_str(), result, thumbnail_cb); gcode.export_layer_filaments(result); //BBS - result->conflict_result = m_conflict_result; + if (result != nullptr) + result->conflict_result = m_conflict_result; return path.c_str(); } diff --git a/tests/data/fff_print_tests/test_gcodewriter/config_lift_unlift.ini b/tests/data/fff_print_tests/test_gcodewriter/config_lift_unlift.ini deleted file mode 100644 index 9d44cd43e7..0000000000 --- a/tests/data/fff_print_tests/test_gcodewriter/config_lift_unlift.ini +++ /dev/null @@ -1,30 +0,0 @@ -before_layer_gcode = -between_objects_gcode = -end_filament_gcode = "; Filament-specific end gcode \n;END gcode for filament\n" -end_gcode = M104 S0 ; turn off temperature\nG28 X0 ; home X axis\nM84 ; disable motors\n -extrusion_axis = E -extrusion_multiplier = 1 -filament_cost = 0 -filament_density = 0 -filament_diameter = 3 -filament_max_volumetric_speed = 0 -gcode_comments = 0 -gcode_flavor = reprap -layer_gcode = -max_print_speed = 80 -max_volumetric_speed = 0 -retract_length = 2 -retract_length_toolchange = 10 -retract_lift = 1.5 -retract_lift_above = 0 -retract_lift_below = 0 -retract_restart_extra = 0 -retract_restart_extra_toolchange = 0 -retract_speed = 40 -start_filament_gcode = "; Filament gcode\n" -start_gcode = G28 ; home all axes\nG1 Z5 F5000 ; lift nozzle\n -toolchange_gcode = -travel_speed = 130 -use_firmware_retraction = 0 -use_relative_e_distances = 0 -use_volumetric_e = 0 diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_data.cpp index 307a279bef..b1096307c8 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_data.cpp @@ -10,10 +10,11 @@ #include #include -#include #include #include +#include "test_utils.hpp" + using namespace std; namespace Slic3r { namespace Test { @@ -282,16 +283,36 @@ void init_and_process_print(std::initializer_list meshes, Slic3r:: std::string gcode(Print & print) { - boost::filesystem::path temp = boost::filesystem::unique_path(); + ScopedTemporaryFile temp(".gcode"); print.set_status_silent(); print.process(); print.export_gcode(temp.string(), nullptr, nullptr); std::ifstream t(temp.string()); std::string str((std::istreambuf_iterator(t)), std::istreambuf_iterator()); - boost::nowide::remove(temp.string().c_str()); return str; } +std::set layers_with_role(const std::string &gcode, const std::string &role) +{ + std::set layers; + GCodeReader parser; + parser.parse_buffer(gcode, [&layers, &role](GCodeReader &self, const GCodeReader::GCodeLine &line) { + if (line.extruding(self) && line.comment().find(role) != std::string_view::npos) + layers.insert(self.z()); + }); + return layers; +} + +double max_z(const std::string &gcode) +{ + double z = 0.0; + GCodeReader parser; + parser.parse_buffer(gcode, [&z](GCodeReader &self, const GCodeReader::GCodeLine &) { + z = std::max(z, static_cast(self.z())); + }); + return z; +} + Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh) { Slic3r::Model result; @@ -338,7 +359,7 @@ std::string slice(std::initializer_list meshes, std::initializer_l #include -SCENARIO("init_print functionality", "[test_data][.]") { +SCENARIO("init_print functionality", "[test_data]") { GIVEN("A default config") { Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); WHEN("init_print is called with a single mesh.") { diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp index 378d7fce7a..721446bc7e 100644 --- a/tests/fff_print/test_data.hpp +++ b/tests/fff_print/test_data.hpp @@ -8,6 +8,8 @@ #include "libslic3r/Print.hpp" #include "libslic3r/TriangleMesh.hpp" +#include +#include #include namespace Slic3r { namespace Test { @@ -80,6 +82,13 @@ std::string slice(std::initializer_list meshes, const DynamicPrint std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); +// Distinct layer Z heights that carry an extrusion tagged with the given role +// comment (requires gcode_comments), e.g. "skirt", "brim", "support". +std::set layers_with_role(const std::string &gcode, const std::string &role); + +// Highest Z reached by any move in the gcode. +double max_z(const std::string &gcode); + } } // namespace Slic3r::Test diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp index 7c0ad00ef1..4419d7f5bf 100644 --- a/tests/fff_print/test_flow.cpp +++ b/tests/fff_print/test_flow.cpp @@ -15,82 +15,7 @@ using namespace Slic3r::Test; using namespace Slic3r; -SCENARIO("Extrusion width specifics", "[Flow][.]") { - GIVEN("A config with a skirt, brim, some fill density, 3 perimeters, and 1 bottom solid layer and a 20mm cube mesh") { - // this is a sharedptr - DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); - config.set_deserialize_strict({ - { "brim_width", 2 }, - { "skirts", 1 }, - { "perimeters", 3 }, - { "fill_density", "40%" }, - { "first_layer_height", 0.3 } - }); - - WHEN("first layer width set to 2mm") { - Slic3r::Model model; - config.set("first_layer_extrusion_width", 2); - Slic3r::Print print; - Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config); - - std::vector E_per_mm_bottom; - std::string gcode = Test::gcode(print); - Slic3r::GCodeReader parser; - const double layer_height = config.opt_float("layer_height"); - parser.parse_buffer(gcode, [&E_per_mm_bottom, layer_height] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) - { - if (self.z() == Catch::Approx(layer_height).margin(0.01)) { // only consider first layer - if (line.extruding(self) && line.dist_XY(self) > 0) { - E_per_mm_bottom.emplace_back(line.dist_E(self) / line.dist_XY(self)); - } - } - }); - THEN(" First layer width applies to everything on first layer.") { - bool pass = false; - double avg_E = std::accumulate(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), 0.0) / static_cast(E_per_mm_bottom.size()); - - pass = (std::count_if(E_per_mm_bottom.cbegin(), E_per_mm_bottom.cend(), [avg_E] (const double& v) { return v == Catch::Approx(avg_E); }) == 0); - REQUIRE(pass == true); - REQUIRE(E_per_mm_bottom.size() > 0); // make sure it actually passed because of extrusion - } - THEN(" First layer width does not apply to upper layer.") { - } - } - } -} -// needs gcode export -SCENARIO(" Bridge flow specifics.", "[Flow]") { - GIVEN("A default config with no cooling and a fixed bridge speed, flow ratio and an overhang mesh.") { - WHEN("bridge_flow_ratio is set to 1.0") { - 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.") { - } - } - } - 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, +/// 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") { diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index ef8fb58b41..2d53dd099d 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -6,68 +6,6 @@ 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 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.lazy_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.z_hop.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.lazy_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.lazy_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.z_hop.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.lazy_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.lazy_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.z_hop.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.lazy_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") { @@ -94,3 +32,28 @@ SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") { } } } + +SCENARIO("z_hop lifts the nozzle when a lift is requested", "[GCodeWriter]") { + GIVEN("A writer with the nozzle parked at Z = 10") { + GCodeWriter writer; + std::vector extruder_ids { 0 }; + writer.set_extruders(extruder_ids); + writer.set_extruder(0); + writer.travel_to_z(10.0); + + WHEN("z_hop is 1 and an eager lift is requested") { + writer.config.z_hop.values = { 1.0 }; + std::string gcode = writer.eager_lift(LiftType::NormalLift); + THEN("a Z move up by z_hop is emitted") { + REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("Z11")); + } + } + WHEN("z_hop is 0") { + writer.config.z_hop.values = { 0.0 }; + std::string gcode = writer.eager_lift(LiftType::NormalLift); + THEN("no lift is emitted") { + REQUIRE(gcode.empty()); + } + } + } +} diff --git a/tests/fff_print/test_model.cpp b/tests/fff_print/test_model.cpp index a802782607..fc25e0eef8 100644 --- a/tests/fff_print/test_model.cpp +++ b/tests/fff_print/test_model.cpp @@ -4,15 +4,15 @@ #include "libslic3r/Model.hpp" #include "libslic3r/ModelArrange.hpp" -#include #include #include "test_data.hpp" +#include "test_utils.hpp" using namespace Slic3r; using namespace Slic3r::Test; -SCENARIO("Model construction", "[Model][.]") { +SCENARIO("Model construction", "[Model]") { GIVEN("A Slic3r Model") { Slic3r::Model model; Slic3r::TriangleMesh sample_mesh = Slic3r::make_cube(20,20,20); @@ -49,12 +49,11 @@ SCENARIO("Model construction", "[Model][.]") { print.set_status_silent(); print.apply(model, config); print.process(); - boost::filesystem::path temp = boost::filesystem::unique_path(); + ScopedTemporaryFile temp(".gcode"); 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()); + REQUIRE(boost::filesystem::exists(temp.path())); + REQUIRE(boost::filesystem::is_regular_file(temp.path())); + REQUIRE(boost::filesystem::file_size(temp.path()) > 0); } } } diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index c94e302e9a..8c59b30292 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -9,35 +9,14 @@ using namespace Slic3r; using namespace Slic3r::Test; -SCENARIO("PrintObject: Perimeter generation", "[PrintObject][.]") { +SCENARIO("Print: Skirt generation", "[Print]") { 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.entities.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") { + WHEN("skirt_loops is set to 2") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { - { "skirt_height", 1 }, - { "skirt_distance", 1 }, - { "skirts", 2 } + { "skirt_height", 1 }, + { "skirt_distance", 1 }, + { "skirt_loops", 2 } }); THEN("Skirt Extrusion collection has 2 loops in it") { REQUIRE(print.skirt().items_count() == 2); @@ -47,19 +26,19 @@ SCENARIO("Print: Skirt generation", "[Print][.]") { } } -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") { +SCENARIO("Print: Changing number of solid shell layers does not cause all surfaces to become internal.", "[Print]") { + GIVEN("sliced 20mm cube and config with top_shell_layers = 2 and bottom_shell_layers = 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 } + { "top_shell_layers", 2 }, + { "bottom_shell_layers", 1 }, + { "layer_height", 0.25 }, // get a known number of layers + { "initial_layer_print_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) + // Precondition: Ensure that the model has 2 solid top layers (79, 78) // 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().at(obj_id)->get_layer((int)layer_id)); @@ -74,8 +53,8 @@ SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces t 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); + WHEN("Model is re-sliced with top_shell_layers == 3") { + config.set("top_shell_layers", 3); print.apply(model, config); print.process(); THEN("Print object does not have 0 solid bottom layers.") { @@ -90,27 +69,14 @@ SCENARIO("Print: Changing number of solid surfaces does not cause all surfaces t } } -SCENARIO("Print: Brim generation", "[Print][.]") { +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") { - size_t total_items = 0; - for (const auto& pair : print.get_brimMap()) { - total_items += pair.second.items_count(); - } - REQUIRE(total_items == 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 } + { "brim_type", "outer_only" }, + { "initial_layer_line_width", 1 }, + { "brim_width", 6 } }); THEN("Brim Extrusion collection has 6 loops in it") { size_t total_items = 0; @@ -123,17 +89,16 @@ SCENARIO("Print: Brim generation", "[Print][.]") { 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 } + { "brim_type", "outer_only" }, + { "brim_width", 6 }, + { "initial_layer_line_width", 0.5 } }); - print.process(); THEN("Brim Extrusion collection has 12 loops in it") { size_t total_items = 0; for (const auto& pair : print.get_brimMap()) { total_items += pair.second.items_count(); } - REQUIRE(total_items == 14); + REQUIRE(total_items == 12); } } } diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp index 180fc6d998..e790c55a93 100644 --- a/tests/fff_print/test_printgcode.cpp +++ b/tests/fff_print/test_printgcode.cpp @@ -14,9 +14,14 @@ #include "libslic3r/GCodeReader.hpp" #include "test_data.hpp" +#include "test_utils.hpp" #include #include +#include +#include +#include +#include using namespace Slic3r; using namespace Slic3r::Test; @@ -25,25 +30,23 @@ 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][.]") { +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", "" } + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "initial_layer_line_width", 0 }, + { "gcode_comments", true }, + { "machine_start_gcode", "" }, + { "z_hop", 0 } }); 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); @@ -61,14 +64,9 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { 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); + THEN("The config trailer includes print and region settings") { REQUIRE(gcode.find("; layer_height") != std::string::npos); - REQUIRE(gcode.find("; fill_density") != std::string::npos); + REQUIRE(gcode.find("; sparse_infill_density") != std::string::npos); } THEN("Infill is emitted.") { boost::smatch has_match; @@ -83,27 +81,22 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { 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(final_z, static_cast(self.z())); // record the highest Z point we reach - }); - REQUIRE(final_z == Catch::Approx(20.)); + REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20., 1e-4)); } } - WHEN("output is executed with complete objects and two differently-sized meshes") { + WHEN("output is executed with two objects printed sequentially") { 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" } + { "initial_layer_line_width", 0 }, + { "initial_layer_print_height", 0.3 }, + { "layer_height", 0.2 }, + { "enable_support", false }, + { "raft_layers", 0 }, + { "print_sequence", "by object" }, + { "gcode_comments", true }, + { "printing_by_object_gcode", "; between-object-gcode" }, + { "z_hop", 0 } }); std::string gcode = Slic3r::Test::gcode(print); THEN("Some text output is generated.") { @@ -125,13 +118,7 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { 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(self.z())); // record the highest Z point we reach - }); - REQUIRE(final_z == Catch::Approx(20.1)); + REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20.1, 1e-4)); } THEN("Z height resets on object change") { double final_z = 0.0; @@ -147,27 +134,13 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { }); 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(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 } + { "initial_layer_line_width", 0 }, + { "enable_support", true }, + { "raft_layers", 3 }, + { "gcode_comments", true } }); THEN("Some text output is generated.") { REQUIRE(gcode.size() > 0); @@ -187,7 +160,7 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { } 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" } + { "initial_layer_line_width", "0.5" } }); THEN("Some text output is generated.") { REQUIRE(gcode.size() > 0); @@ -204,18 +177,18 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { } WHEN("Cooling is enabled and the fan is disabled.") { std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, { - { "cooling", true }, - { "disable_fan_first_layers", 5 } + { "cooling", true }, + { "close_fan_the_first_x_layers", 5 } }); THEN("GCode to disable fan is emitted."){ - REQUIRE(gcode.find("M107") != std::string::npos); + REQUIRE(gcode.find("M106 S0") != 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 } + { "machine_end_gcode", "; Layer_num [layer_num]\n; Layer_z [layer_z]" }, + { "layer_height", 0.1 }, + { "initial_layer_print_height", 0.1 } }); THEN("layer_num and layer_z are processed in the end gcode") { REQUIRE(gcode.find("; Layer_num 199") != std::string::npos); @@ -223,39 +196,21 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { } } 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); - } + std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, { + { "machine_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); } } 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 } + { "print_sequence", "by object" }, + { "gcode_comments", true }, + { "layer_change_gcode", ";Layer:[layer_num] ([layer_z] mm)" }, + { "layer_height", 0.1 }, + { "initial_layer_print_height", 0.1 } }); // End of the 1st object. std::string token = ";Layer:199 "; @@ -267,15 +222,82 @@ SCENARIO( "PrintGCode basic functionality", "[PrintGCode][.]") { REQUIRE(pos < gcode.size()); double z = 0; REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1)); - REQUIRE(z == Catch::Approx(20.)); + REQUIRE_THAT(z, Catch::Matchers::WithinAbs(20., 1e-4)); // 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 == Catch::Approx(20.)); + REQUIRE_THAT(z, Catch::Matchers::WithinAbs(20., 1e-4)); } } } } + +TEST_CASE("export_gcode writes G-code without a result pointer", "[PrintGCode][export_gcode]") +{ + Print print; + Model model; + Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model); + print.process(); + + SECTION("non-BBL printer") {} + SECTION("BBL printer") { print.is_BBL_printer() = true; } + + ScopedTemporaryFile temp(".gcode"); + REQUIRE_NOTHROW(print.export_gcode(temp.string(), nullptr, nullptr)); + + std::ifstream in(temp.string()); + const std::string gcode((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + + REQUIRE_FALSE(gcode.empty()); +} + +TEST_CASE("Initial layer height is honored", "[PrintGCode]") +{ + const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, { + { "initial_layer_print_height", 0.3 }, + { "layer_height", 0.2 }, + { "z_hop", 0 } // keep recorded Z equal to the printed layer height + }); + + std::set layer_zs; + GCodeReader reader; + reader.parse_buffer(gcode, [&layer_zs] (GCodeReader& self, const GCodeReader::GCodeLine& line) { + if (line.extruding(self) && line.dist_XY(self) > 0) + layer_zs.insert(self.z()); + }); + + REQUIRE(layer_zs.size() > 1); + REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4)); + REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4)); +} + +TEST_CASE("Sequential printing follows model order", "[PrintGCode]") +{ + // Two objects of different heights, taller one added first. Orca prints + // sequential objects in model order, so the taller one is printed first. + const std::string gcode = Slic3r::Test::slice({ Slic3r::make_cube(20, 20, 20), Slic3r::make_cube(20, 20, 10) }, { + { "print_sequence", "by object" }, + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "z_hop", 0 } + }); + + // The first object's height is the peak Z reached before Z drops back to the + // first layer (the object change). With by-object printing only an object + // change returns Z to the bottom. + double first_object_peak_z = 0.0; + double running_peak = 0.0; + GCodeReader reader; + reader.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) { + if (first_object_peak_z != 0.0 || !line.extruding(self)) return; // ignore travels (e.g. start-gcode Z lift) + if (running_peak > 1.0 && self.z() < 1.0) + first_object_peak_z = running_peak; + else + running_peak = std::max(running_peak, static_cast(self.z())); + }); + + REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3)); +} diff --git a/tests/fff_print/test_printobject.cpp b/tests/fff_print/test_printobject.cpp index 88b88826a3..c569384acb 100644 --- a/tests/fff_print/test_printobject.cpp +++ b/tests/fff_print/test_printobject.cpp @@ -9,14 +9,14 @@ 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") { +SCENARIO("PrintObject: object layer heights", "[PrintObject]") { + GIVEN("A 20mm cube") { + WHEN("sliced with a 2mm layer height and a 3mm nozzle") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { - { "first_layer_height", 2 }, - { "layer_height", 2 }, - { "nozzle_diameter", 3 } + { "initial_layer_print_height", 2 }, + { "layer_height", 2 }, + { "nozzle_diameter", 3 } }); ConstLayerPtrsAdaptor layers = print.objects().front()->layers(); THEN("The output vector has 10 entries") { @@ -25,65 +25,84 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject][.]") { 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) == Catch::Approx(2.0)); + REQUIRE_THAT(layers[i]->print_z - last, Catch::Matchers::WithinAbs(2.0, 1e-4)); last = layers[i]->print_z; } } } - WHEN("generate_object_layers() is called for 10mm layer heights and nozzle diameter of 11mm") { + WHEN("sliced with a 10mm layer height and an 11mm nozzle") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { - { "first_layer_height", 2 }, - { "layer_height", 10 }, - { "nozzle_diameter", 11 } + { "initial_layer_print_height", 2 }, + { "layer_height", 10 }, + { "nozzle_diameter", 11 } }); ConstLayerPtrsAdaptor 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 == Catch::Approx(2.0)); + REQUIRE_THAT(layers.front()->print_z, Catch::Matchers::WithinAbs(2.0, 1e-4)); } AND_THEN("Layer 1 is at 12mm") { - REQUIRE(layers[1]->print_z == Catch::Approx(12.0)); + REQUIRE_THAT(layers[1]->print_z, Catch::Matchers::WithinAbs(12.0, 1e-4)); } } - WHEN("generate_object_layers() is called for 15mm layer heights and nozzle diameter of 16mm") { + WHEN("sliced with a 15mm layer height and a 16mm nozzle") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { - { "first_layer_height", 2 }, - { "layer_height", 15 }, - { "nozzle_diameter", 16 } + { "initial_layer_print_height", 2 }, + { "layer_height", 15 }, + { "nozzle_diameter", 16 } }); ConstLayerPtrsAdaptor 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 == Catch::Approx(2.0)); + REQUIRE_THAT(layers[0]->print_z, Catch::Matchers::WithinAbs(2.0, 1e-4)); } AND_THEN("Layer 1 is at 17mm") { - REQUIRE(layers[1]->print_z == Catch::Approx(17.0)); + REQUIRE_THAT(layers[1]->print_z, Catch::Matchers::WithinAbs(17.0, 1e-4)); } } -#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 &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) == Catch::Approx(5.0)); - last = layers[i]->print_z; - } + WHEN("layer height exceeds the nozzle diameter") { + // Orca does not clamp an over-large layer height to the nozzle; it + // rejects the slice during flow computation. Pin that behavior. + THEN("Slicing is rejected") { + Slic3r::Print print; + REQUIRE_THROWS(Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { + { "initial_layer_print_height", 0.3 }, + { "layer_height", 0.5 }, + { "nozzle_diameter", 0.4 } + })); + } + } + } +} + +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, { { "sparse_infill_density", 0 } }); + const PrintObject &object = *print.objects().front(); + THEN("Every layer in region 0 has 1 island of perimeters") { + for (const Layer *layer : object.layers()) + REQUIRE(layer->regions().front()->perimeters.entities.size() == 1); + } + } + WHEN("wall_loops is set to 3") { + Slic3r::Print print; + Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { + { "sparse_infill_density", 0 }, + { "wall_loops", 3 } + }); + const PrintObject &object = *print.objects().front(); + THEN("Every layer in region 0 has 3 perimeter loops") { + for (const Layer *layer : object.layers()) + REQUIRE(layer->regions().front()->perimeters.items_count() == 3); } } -#endif } } diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 88032730c7..e274915d61 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -3,16 +3,19 @@ #include "libslic3r/GCodeReader.hpp" #include "libslic3r/Config.hpp" #include "libslic3r/Geometry.hpp" +#include "libslic3r/Geometry/ConvexHull.hpp" #include +#include + #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) +/// Helper method to find the tool used for the brim (always the first extrusion). +[[maybe_unused]] static int get_brim_tool(const std::string &gcode) { int brim_tool = -1; int tool = -1; @@ -29,16 +32,13 @@ static int get_brim_tool(const std::string &gcode) return brim_tool; } -TEST_CASE("Skirt height is honored", "[Skirt][.]") { +TEST_CASE("Skirt height is honored", "[SkirtBrim]") { 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 }, - { "first_layer_speed", "100%" } + { "skirt_loops", 1 }, + { "skirt_height", 5 }, + { "wall_loops", 0 }, + { "gcode_comments", true } }); std::string gcode; @@ -49,75 +49,38 @@ TEST_CASE("Skirt height is honored", "[Skirt][.]") { gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, config); } - std::map layers_with_skirt; - double support_speed = config.opt("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() == Catch::Approx(support_speed)) { - layers_with_skirt[self.z()] = 1; - } - }); - REQUIRE(layers_with_skirt.size() == (size_t)config.opt_int("skirt_height")); + REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t)config.opt_int("skirt_height")); } -SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim][.]") { +SCENARIO("Skirt and brim generation", "[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 }, + { "initial_layer_print_height", 0.3 }, + { "gcode_comments", true }, // avoid altering speeds unexpectedly - { "cooling", false }, - { "first_layer_speed", "100%" }, + { "slow_down_for_layer_cooling", false }, + { "initial_layer_speed", "100%" }, // remove noise from top/solid layers - { "top_solid_layers", 0 }, - { "bottom_solid_layers", 1 }, - { "start_gcode", "T[initial_tool]\n" } + { "top_shell_layers", 0 }, + { "bottom_shell_layers", 1 }, + { "machine_start_gcode", "T[initial_tool]\n" } }); WHEN("Brim width is set to 5") { config.set_deserialize_strict({ - { "perimeters", 0 }, - { "skirts", 0 }, - { "brim_width", 5 } + { "wall_loops", 0 }, + { "skirt_loops", 0 }, + { "brim_type", "outer_only" }, + { "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("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() == Catch::Approx(0.3) || line.new_Z(self) == Catch::Approx(0.3)) { - if (line.extruding(self) && self.f() == Catch::Approx(support_speed)) { - brim_generated = true; - } - } - }); - REQUIRE(brim_generated); + REQUIRE(! layers_with_role(gcode, "brim").empty()); } } - 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! @@ -154,10 +117,11 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim][.]") { 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 } - }); + { "skirt_loops", 0 }, + { "initial_layer_line_width", 0.5 }, + { "brim_type", "outer_only" }, + { "brim_width", 1 } + }); THEN("2 brim lines") { Slic3r::Print print; Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config); @@ -203,68 +167,68 @@ SCENARIO("Original Slic3r Skirt/Brim tests", "[SkirtBrim][.]") { 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" } + { "layer_height", 0.4 }, + { "initial_layer_print_height", 0.4 }, + { "skirt_loops", 1 }, + { "skirt_distance", 0 }, + { "enable_support", 1 }, + { "brim_type", "outer_only" }, + { "brim_width", 5 } }); - THEN("overhang generates?") { - //FIXME does it make sense? - REQUIRE(! Slic3r::Test::slice({TestMesh::overhang}, config).empty()); + THEN("Support and brim are both emitted") { + std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); + REQUIRE(! layers_with_role(gcode, "support").empty()); + REQUIRE(! layers_with_role(gcode, "brim").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("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() == Catch::Approx(config.opt("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 == Catch::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() == Catch::Approx(0.3) || line.new_Z(self) == Catch::Approx(0.3)) { - if (line.extruding(self) && self.f() == Catch::Approx(support_speed)) { - } - } - }); - Slic3r::Polygon convex_hull = Slic3r::Geometry::convex_hull(extrusion_points); - double hull_perimeter = unscale(convex_hull.split_at_first_point().length()); - REQUIRE(skirt_length > hull_perimeter); - } -#endif - + } + WHEN("an object with support is surrounded by a skirt") { + config.set_deserialize_strict({ + { "enable_support", 1 }, + { "skirt_loops", 1 }, + { "skirt_distance", 2 }, + { "brim_type", "no_brim" }, + { "z_hop", 0 } + }); + THEN("the skirt is long enough to enclose the object and its support") { + std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); + const double first_layer_z = config.opt_float("initial_layer_print_height"); + + // On the first layer, accumulate the skirt loop length and collect the + // object + support extrusion points; the skirt must enclose them. + double skirt_length = 0.0; + Points footprint; + GCodeReader parser; + parser.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) { + if (! line.extruding(self) || line.dist_XY(self) <= 0 || std::abs(self.z() - first_layer_z) > 0.01) + return; + if (line.comment().find("skirt") != std::string_view::npos) + skirt_length += line.dist_XY(self); + else + footprint.push_back(Point::new_scale(line.new_X(self), line.new_Y(self))); + }); + + const double hull_perimeter = unscale(Geometry::convex_hull(footprint).split_at_first_point().length()); + REQUIRE(hull_perimeter > 0.0); // guard against an empty footprint passing trivially + REQUIRE(skirt_length > hull_perimeter); + } } 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()); + // One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops. + config.set_deserialize_strict({ + { "skirt_loops", 1 }, + { "min_skirt_length", 500 } + }); + THEN("The skirt is extended to at least the minimum length") { + std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); + double skirt_length = 0.0; + GCodeReader parser; + parser.parse_buffer(gcode, [&skirt_length] (GCodeReader& self, const GCodeReader::GCodeLine& line) { + if (line.extruding(self) && line.comment().find("skirt") != std::string_view::npos) + skirt_length += line.dist_XY(self); + }); + REQUIRE(skirt_length >= 500.0); } } } diff --git a/tests/fff_print/test_support_material.cpp b/tests/fff_print/test_support_material.cpp index d61f97ca81..d1fc9d66e4 100644 --- a/tests/fff_print/test_support_material.cpp +++ b/tests/fff_print/test_support_material.cpp @@ -8,24 +8,41 @@ using namespace Slic3r::Test; using namespace Slic3r; -TEST_CASE("SupportMaterial: Three raft layers created", "[SupportMaterial][.]") +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 } + { "enable_support", 1 }, + { "raft_layers", 3 } }); REQUIRE(print.objects().front()->support_layers().size() == 3); } -SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMaterial][.]") +TEST_CASE("SupportMaterial: enforced support layers are generated", "[SupportMaterial]") +{ + // enforce_support_layers forces support on the first N layers even with support off. + Slic3r::Print baseline; + Slic3r::Test::init_and_process_print({ TestMesh::overhang }, baseline, { + { "enable_support", 0 }, + { "enforce_support_layers", 0 } + }); + REQUIRE(baseline.objects().front()->support_layers().empty()); + + Slic3r::Print enforced; + Slic3r::Test::init_and_process_print({ TestMesh::overhang }, enforced, { + { "enable_support", 0 }, + { "enforce_support_layers", 100 } + }); + REQUIRE(enforced.objects().front()->support_layers().size() > 0); +} + +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) + auto check = [](Slic3r::Print &print, bool &first_support_layer_height_ok, bool &layer_height_minimum_ok, bool &layer_height_maximum_ok) { ConstSupportLayerPtrsAdaptor support_layers = print.objects().front()->support_layers(); @@ -43,196 +60,36 @@ SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMate 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 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(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") { + WHEN("Layer height = 0.2 and 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 }, + { "enable_support", 1 }, + { "layer_height", 0.2 }, + { "initial_layer_print_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); } + bool first_layer_ok, layer_min_ok, layer_max_ok; + check(print, first_layer_ok, layer_min_ok, layer_max_ok); + THEN("First layer height is honored") { REQUIRE(first_layer_ok == true); } + THEN("No null or negative support layers") { REQUIRE(layer_min_ok == true); } + THEN("No layers thicker than nozzle diameter") { REQUIRE(layer_max_ok == true); } } - WHEN("Layer height = 0.2 and, first layer height = 0.3") { + 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 }, + { "enable_support", 1 }, + { "layer_height", 0.2 }, + { "initial_layer_print_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); } + bool first_layer_ok, layer_min_ok, layer_max_ok; + check(print, first_layer_ok, layer_min_ok, layer_max_ok); + THEN("First layer height is honored") { REQUIRE(first_layer_ok == true); } + THEN("No null or negative support layers") { REQUIRE(layer_min_ok == true); } + THEN("No layers thicker than nozzle diameter") { REQUIRE(layer_max_ok == 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 contact_z = {1.9}; - std::vector 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 diff --git a/tests/test_utils.hpp b/tests/test_utils.hpp index 3cdd717643..7fe55c5333 100644 --- a/tests/test_utils.hpp +++ b/tests/test_utils.hpp @@ -4,6 +4,8 @@ #include #include +#include + #if defined(WIN32) || defined(_WIN32) #define PATH_SEPARATOR R"(\)" #else @@ -20,4 +22,26 @@ inline Slic3r::TriangleMesh load_model(const std::string &obj_filename) return mesh; } +// RAII holder for a unique temporary file path, removed when the guard goes out +// of scope so a failing assertion never leaks it. Uses the system temp dir with +// a unique name (parallel-safe, cross-platform). The file itself is created by +// whoever writes to path()/string(); this only reserves the name and cleans up. +class ScopedTemporaryFile +{ +public: + explicit ScopedTemporaryFile(const std::string &extension = ".tmp") + : m_path(boost::filesystem::temp_directory_path() + / boost::filesystem::unique_path("orca-%%%%-%%%%-%%%%" + extension)) + {} + ~ScopedTemporaryFile() { boost::system::error_code ec; boost::filesystem::remove(m_path, ec); } + ScopedTemporaryFile(const ScopedTemporaryFile &) = delete; + ScopedTemporaryFile &operator=(const ScopedTemporaryFile &) = delete; + + const boost::filesystem::path &path() const { return m_path; } + std::string string() const { return m_path.string(); } + +private: + boost::filesystem::path m_path; +}; + #endif // SLIC3R_TEST_UTILS