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
This commit is contained in:
raistlin7447
2026-06-14 04:42:53 -05:00
committed by GitHub
parent abb4eddb9c
commit 5fafbb59fc
16 changed files with 419 additions and 676 deletions

View File

@@ -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;

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -10,10 +10,11 @@
#include <cstdlib>
#include <string>
#include <boost/nowide/cstdio.hpp>
#include <boost/filesystem.hpp>
#include <libslic3r/ModelArrange.hpp>
#include "test_utils.hpp"
using namespace std;
namespace Slic3r { namespace Test {
@@ -282,16 +283,36 @@ void init_and_process_print(std::initializer_list<TriangleMesh> 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<char>(t)), std::istreambuf_iterator<char>());
boost::nowide::remove(temp.string().c_str());
return str;
}
std::set<double> layers_with_role(const std::string &gcode, const std::string &role)
{
std::set<double> 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<double>(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<TriangleMesh> meshes, std::initializer_l
#include <catch2/catch_all.hpp>
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.") {

View File

@@ -8,6 +8,8 @@
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <set>
#include <string>
#include <unordered_map>
namespace Slic3r { namespace Test {
@@ -80,6 +82,13 @@ std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrint
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);
// Distinct layer Z heights that carry an extrusion tagged with the given role
// comment (requires gcode_comments), e.g. "skirt", "brim", "support".
std::set<double> 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

View File

@@ -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<double> 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<double>(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") {

View File

@@ -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<unsigned int> extruder_ids {0};
writer.set_extruders(extruder_ids);
writer.set_extruder(0);
WHEN("Z is set to 203") {
double trouble_Z = 203;
writer.travel_to_z(trouble_Z);
AND_WHEN("GcodeWriter::Lift() is called") {
REQUIRE(writer.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<unsigned int> 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());
}
}
}
}

View File

@@ -4,15 +4,15 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include <boost/nowide/cstdio.hpp>
#include <boost/filesystem.hpp>
#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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -14,9 +14,14 @@
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <boost/regex.hpp>
#include <boost/filesystem.hpp>
#include <fstream>
#include <iterator>
#include <set>
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<double>(final_z, static_cast<double>(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<double>(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<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
}
WHEN("the output is executed with support material") {
std::string gcode = ::Test::slice({TestMesh::cube_20x20x20}, {
{ "first_layer_extrusion_width", 0 },
{ "support_material", true },
{ "raft_layers", 3 },
{ "gcode_comments", true }
{ "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<char>(in)), std::istreambuf_iterator<char>());
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<double> 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<double>(self.z()));
});
REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3));
}

View File

@@ -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<Slic3r::Layer*> &layers = print.objects().front()->layers();
THEN("The layer height is limited to 5mm.") {
CHECK(layers.size() == 5);
coordf_t last = 2.0;
for (size_t i = 1; i < layers.size(); i++) {
REQUIRE((layers[i]->print_z - last) == 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
}
}

View File

@@ -3,16 +3,19 @@
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Geometry/ConvexHull.hpp"
#include <boost/algorithm/string.hpp>
#include <cmath>
#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<double, bool> layers_with_skirt;
double support_speed = config.opt<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
GCodeReader parser;
parser.parse_buffer(gcode, [&layers_with_skirt, &support_speed] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) {
if (line.extruding(self) && self.f() == 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<Slic3r::ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
Slic3r::GCodeReader parser;
parser.parse_buffer(gcode, [&brim_generated, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
if (self.z() == 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<ConfigOptionFloat>("support_material_speed")->value * MM_PER_MIN;
double skirt_length = 0.0;
Points extrusion_points;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [config, &extrusion_points, &tool, &skirt_length, support_speed] (Slic3r::GCodeReader& self, const Slic3r::GCodeReader::GCodeLine& line) {
// std::cerr << line.cmd() << "\n";
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (self.z() == Catch::Approx(config.opt<ConfigOptionFloat>("first_layer_height")->value)) {
// on first layer
if (line.extruding(self) && line.dist_XY(self) > 0) {
float speed = ( self.f() > 0 ? self.f() : line.new_F(self));
// std::cerr << "Tool " << tool << "\n";
if (speed == 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<double>(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<double>(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);
}
}
}

View File

@@ -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<coordf_t> top_z { 1.1 };
for (coordf_t top_z_el : top_z) {
// find layer index of this top surface.
size_t layer_id = -1;
for (size_t i = 0; i < support_z.size(); ++ i) {
if (abs(support_z[i] - top_z_el) < EPSILON) {
layer_id = i;
i = static_cast<int>(support_z.size());
}
}
// check that first support layer above this top surface (or the next one) is spaced with nozzle diameter
if (abs(support_z[layer_id + 1] - support_z[layer_id] - expected_top_spacing) > EPSILON &&
abs(support_z[layer_id + 2] - support_z[layer_id] - expected_top_spacing) > EPSILON) {
wrong_top_spacing = 1;
}
}
d = ! wrong_top_spacing;
#else
top_spacing_ok = true;
#endif
};
GIVEN("A print object having one modelObject") {
WHEN("First layer height = 0.4") {
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<coordf_t> contact_z = {1.9};
std::vector<coordf_t> top_z = {1.1};
print.default_object_config.support_material_enforce_layers = 100;
print.default_object_config.support_material = 0;
print.default_object_config.layer_height = 0.2;
print.default_object_config.set_deserialize("first_layer_height", "0.3");
print.add_model_object(model.objects[0]);
print.objects.front()->_slice();
SupportMaterial *support = print.objects.front()->_support_material();
auto support_z = support->support_layers_z(contact_z, top_z, print.default_object_config.layer_height);
bool check = true;
for (size_t i = 1; i < support_z.size(); i++) {
if (support_z[i] - support_z[i - 1] <= 0)
check = false;
}
REQUIRE(check == true);
}
// TODO
bool test_6_checks(Print& print)
{
bool has_bridge_speed = true;
// Pre-Processing.
PrintObject* print_object = print.objects.front();
print_object->infill();
SupportMaterial* support_material = print.objects.front()->_support_material();
support_material->generate(print_object);
// TODO but not needed in test 6 (make brims and make skirts).
// Exporting gcode.
// TODO validation found in Simple.pm
return has_bridge_speed;
}
// Test 6.
SCENARIO("SupportMaterial: Checking bridge speed", "[SupportMaterial]")
{
GIVEN("Print object") {
// Create a mesh & modelObject.
TriangleMesh mesh = TriangleMesh::make_cube(20, 20, 20);
Model model = Model();
ModelObject *object = model.add_object();
object->add_volume(mesh);
model.add_default_instances();
model.align_instances_to_origin();
Print print = Print();
print.config.brim_width = 0;
print.config.skirts = 0;
print.config.skirts = 0;
print.default_object_config.support_material = 1;
print.default_region_config.top_solid_layers = 0; // so that we don't have the internal bridge over infill.
print.default_region_config.bridge_speed = 99;
print.config.cooling = 0;
print.config.set_deserialize("first_layer_speed", "100%");
WHEN("support_material_contact_distance = 0.2") {
print.default_object_config.support_material_contact_distance = 0.2;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0") {
print.default_object_config.support_material_contact_distance = 0;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
WHEN("support_material_contact_distance = 0.2 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0.2;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is used.
}
WHEN("support_material_contact_distance = 0 & raft_layers = 5") {
print.default_object_config.support_material_contact_distance = 0;
print.default_object_config.raft_layers = 5;
print.add_model_object(model.objects[0]);
bool check = test_6_checks(print);
REQUIRE(check == true); // bridge speed is not used.
}
}
}
#endif

View File

@@ -4,6 +4,8 @@
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/Format/OBJ.hpp>
#include <boost/filesystem.hpp>
#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