mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 01:41:03 +00:00
Merge upstream/main into iXex PR branch (#13086)
Catches the iXex/IDEX parallel-printing branch up to upstream main (102 commits). Two content conflicts resolved: - src/libslic3r/Preset.cpp: s_Preset_printer_options — kept upstream's new "use_3mf" key and our iMEX printer-capability/mode keys. - tests/fff_print/test_gcodewriter.cpp: upstream revived the disabled suite (#14196), dropping the obsolete [.]-tagged lift() test and its config_lift_unlift.ini; kept their set_speed + z_hop tests and appended our 10 per-firmware set_pressure_advance/set_temperature scenarios. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+22
-24
@@ -98,9 +98,9 @@ REQUIRE_THAT(calculated_value, WithinULP(expected, 4)); // 4 ULPs apart
|
||||
|
||||
## Overview of OrcaSlicer's Testing Framework
|
||||
|
||||
OrcaSlicer uses **Catch2 v2** as its primary testing framework. The test suite is organized into several modules that mirror the project's architectural components:
|
||||
OrcaSlicer uses **Catch2 v3** (currently v3.11.0, vendored in `tests/catch2/`) as its primary testing framework. The test suite is organized into several modules that mirror the project's architectural components:
|
||||
|
||||
> **Note**: OrcaSlicer currently uses Catch2 v2 (based on `#include <catch2/catch.hpp>` includes). Some features mentioned in this guide are only available in v3 and marked accordingly.
|
||||
> **Note**: Test files include the framework via `#include <catch2/catch_all.hpp>` (the v3 single-header convenience include). All v3 features described in this guide are available.
|
||||
|
||||
### Test Structure
|
||||
```
|
||||
@@ -149,13 +149,13 @@ Stereolithography specific tests:
|
||||
|
||||
### File Organization
|
||||
1. **Naming Convention**: `test_<feature>.cpp` (e.g., `test_geometry.cpp`)
|
||||
2. **Header Structure**: Include `<catch2/catch.hpp>` first, then relevant headers
|
||||
2. **Header Structure**: Include `<catch2/catch_all.hpp>` first, then relevant headers
|
||||
3. **Namespace Usage**: Use `using namespace Slic3r;` for convenience
|
||||
4. **File Placement**: Add to appropriate test directory and update CMakeLists.txt
|
||||
|
||||
### Test Naming and Structure
|
||||
```cpp
|
||||
#include <catch2/catch.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -216,7 +216,7 @@ REQUIRE_THROWS_MATCHES(function_call(), SpecificException,
|
||||
// String matchers
|
||||
using Catch::Matchers::StartsWith;
|
||||
using Catch::Matchers::EndsWith;
|
||||
using Catch::Matchers::ContainsSubstring; // Note: v2 uses "Contains"
|
||||
using Catch::Matchers::ContainsSubstring;
|
||||
using Catch::Matchers::Equals;
|
||||
using Catch::Matchers::Matches; // Regex matching
|
||||
|
||||
@@ -330,7 +330,7 @@ TEST_CASE_METHOD(GeometryFixture, "Point operations", "[Geometry]") {
|
||||
REQUIRE(origin.distance_to(unit_x) == 1.0);
|
||||
}
|
||||
|
||||
// Persistent fixture - single instance for entire test case (v2.12.0+)
|
||||
// Persistent fixture - single instance for entire test case (v3.2.0+)
|
||||
TEST_CASE_PERSISTENT_FIXTURE(GeometryFixture, "Persistent operations", "[Geometry]") {
|
||||
static int call_count = 0;
|
||||
++call_count;
|
||||
@@ -384,8 +384,7 @@ TEST_CASE("Explicit test control", "[Control]") {
|
||||
WARN("This warns but doesn't fail the test");
|
||||
|
||||
if (precondition_not_met) {
|
||||
// SKIP("Reason"); // v3.3.0+ only, not available in v2
|
||||
SUCCEED("Test cannot run due to precondition"); // v2 alternative
|
||||
SKIP("Reason"); // Marks the test as skipped (v3.3.0+, available)
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -506,7 +505,7 @@ TEST_CASE("Algorithm performance", "[Performance][Algorithm]") {
|
||||
// Large test data
|
||||
std::vector<Point> points = generate_large_point_set(10000);
|
||||
|
||||
// Time the operation (manual timing for Catch2 v2)
|
||||
// Time the operation (manual timing example; the BENCHMARK macro is also available)
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
auto result = convex_hull(points);
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
@@ -746,7 +745,7 @@ REQUIRE_THROWS_AS(risky_function(), SpecificException);
|
||||
|
||||
⚠️ **CRITICAL**: Catch2 assertions are **NOT thread-safe** by default!
|
||||
|
||||
> **Note**: Catch2 v3.9.0+ has opt-in thread-safe assertions via `CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS`, but OrcaSlicer uses v2
|
||||
> **Note**: Catch2 v3.9.0+ has opt-in thread-safe assertions via `CATCH_CONFIG_EXPERIMENTAL_THREAD_SAFE_ASSERTIONS`. OrcaSlicer is on v3.11.0 but does not enable this flag, so assertions remain non-thread-safe by default.
|
||||
|
||||
❌ **Incorrect**: Will cause undefined behavior or crashes
|
||||
```cpp
|
||||
@@ -812,7 +811,7 @@ TEST_CASE("Resource management", "[Memory]") {
|
||||
### Runtime Performance
|
||||
```cpp
|
||||
TEST_CASE("Performance-sensitive test", "[Performance]") {
|
||||
// Manual timing for Catch2 v2 (v3 has built-in benchmarking)
|
||||
// Manual timing example (Catch2's built-in BENCHMARK macro is also available)
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto result = expensive_operation();
|
||||
@@ -921,21 +920,20 @@ std::foo_function(); // Always call qualified
|
||||
// NOT: #include <foo.h> and foo_function();
|
||||
```
|
||||
|
||||
### Catch2 Version-Specific Limitations
|
||||
### Catch2 v3 Features Available
|
||||
```cpp
|
||||
// OrcaSlicer uses Catch2 v2 - these features are NOT available:
|
||||
// SKIP() macro - Available in v3.3.0+
|
||||
// Thread-safe assertions - Available in v3.9.0+
|
||||
// BENCHMARK improvements - Many in v3.x
|
||||
// testCasePartial events - Available in v3.0.1+
|
||||
// Multiple reporters - Available in v3.0.1+
|
||||
// STATIC_CHECK macro - Available in v3.0.1+
|
||||
// OrcaSlicer is on Catch2 v3.11.0 - all of these ARE available:
|
||||
// SKIP() macro - v3.3.0+
|
||||
// Opt-in thread-safe assertions - v3.9.0+ (NOT enabled here; see Thread Safety)
|
||||
// Built-in BENCHMARK / BENCHMARK_ADVANCED - v3.x
|
||||
// testCasePartial events - v3.0.1+
|
||||
// Multiple reporters simultaneously - v3.0.1+
|
||||
// STATIC_CHECK macro - v3.0.1+
|
||||
// Built-in test sharding (--shard-*) - v3.x
|
||||
|
||||
// v2 Limitations to remember:
|
||||
// - Sections can be re-run if last section fails
|
||||
// - String matcher is "Contains" not "ContainsSubstring"
|
||||
// - Limited benchmarking support compared to v3
|
||||
// - No test sharding built-in
|
||||
// v3 notes to remember:
|
||||
// - String matcher is "ContainsSubstring" (v2's "Contains" no longer exists)
|
||||
// - Sections can still be re-run if a later section fails (unchanged from v2)
|
||||
```
|
||||
|
||||
### Test Organization Best Practices
|
||||
|
||||
@@ -47,6 +47,11 @@ function(orcaslicer_copy_test_dlls)
|
||||
endforeach()
|
||||
endfunction()
|
||||
|
||||
# Register Catch2 tags as CTest labels so `ctest -L`/`-LE` can filter by tag.
|
||||
function(orcaslicer_discover_tests TARGET)
|
||||
catch_discover_tests(${TARGET} ADD_TAGS_AS_LABELS)
|
||||
endfunction()
|
||||
|
||||
add_subdirectory(libnest2d)
|
||||
add_subdirectory(libslic3r)
|
||||
add_subdirectory(slic3rutils)
|
||||
|
||||
@@ -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
|
||||
@@ -21,4 +21,4 @@ set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
@@ -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.") {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -6,65 +6,55 @@
|
||||
|
||||
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);
|
||||
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
|
||||
|
||||
std::vector<unsigned int> extruder_ids {0};
|
||||
GIVEN("GCodeWriter instance") {
|
||||
GCodeWriter writer;
|
||||
WHEN("set_speed is called to set speed to 99999.123") {
|
||||
THEN("Output string is G1 F99999.123") {
|
||||
REQUIRE_THAT(writer.set_speed(99999.123), Catch::Matchers::Equals("G1 F99999.123\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 1") {
|
||||
THEN("Output string is G1 F1") {
|
||||
REQUIRE_THAT(writer.set_speed(1.0), Catch::Matchers::Equals("G1 F1\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 203.200022") {
|
||||
THEN("Output string is G1 F203.2") {
|
||||
REQUIRE_THAT(writer.set_speed(203.200022), Catch::Matchers::Equals("G1 F203.2\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 203.200522") {
|
||||
THEN("Output string is G1 F203.201") {
|
||||
REQUIRE_THAT(writer.set_speed(203.200522), Catch::Matchers::Equals("G1 F203.201\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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_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 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_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());
|
||||
}
|
||||
}
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,30 +321,3 @@ SCENARIO("set_pressure_advance emits BBL M900 L1000 M10 regardless of tool index
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
|
||||
|
||||
GIVEN("GCodeWriter instance") {
|
||||
GCodeWriter writer;
|
||||
WHEN("set_speed is called to set speed to 99999.123") {
|
||||
THEN("Output string is G1 F99999.123") {
|
||||
REQUIRE_THAT(writer.set_speed(99999.123), Catch::Matchers::Equals("G1 F99999.123\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 1") {
|
||||
THEN("Output string is G1 F1") {
|
||||
REQUIRE_THAT(writer.set_speed(1.0), Catch::Matchers::Equals("G1 F1\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 203.200022") {
|
||||
THEN("Output string is G1 F203.2") {
|
||||
REQUIRE_THAT(writer.set_speed(203.200022), Catch::Matchers::Equals("G1 F203.2\n"));
|
||||
}
|
||||
}
|
||||
WHEN("set_speed is called to set speed to 203.200522") {
|
||||
THEN("Output string is G1 F203.201") {
|
||||
REQUIRE_THAT(writer.set_speed(203.200522), Catch::Matchers::Equals("G1 F203.201\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+188
-57
@@ -3,41 +3,23 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
|
||||
#include "test_data.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
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 +29,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 +56,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 +72,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,18 +92,180 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Print::validate() warning collection
|
||||
//
|
||||
// validate() returns its warnings in a vector. The warning paths deliberately
|
||||
// differ in how many entries they produce; these tests pin down each behaviour:
|
||||
// * independent checks -> stack (one entry each)
|
||||
// * motion-ability -> coalesce into one (mutually exclusive, gated)
|
||||
// * clumping detection -> one independent warning
|
||||
// * layered clearance -> many collisions concatenated into one entry
|
||||
// * null warnings pointer -> no-op, no crash, no blocking error
|
||||
// ---------------------------------------------------------------------------
|
||||
namespace {
|
||||
|
||||
// Build `n` 20mm cubes (spread apart, or stacked at the origin when `overlap`) into
|
||||
// `model`/`print` and apply `config`, leaving the print ready to validate(). No slicing needed.
|
||||
void build_cubes(Slic3r::Model& model, Slic3r::Print& print,
|
||||
DynamicPrintConfig config, int n, bool overlap)
|
||||
{
|
||||
config.set_key_value("layer_change_gcode", new ConfigOptionString("G92 E0\n")); // validate() relative-E reset
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
ModelObject* object = model.add_object();
|
||||
object->add_volume(Slic3r::Test::mesh(TestMesh::cube_20x20x20));
|
||||
ModelInstance* inst = object->add_instance();
|
||||
inst->set_offset(Vec3d(overlap ? 0.0 : i * 60.0, 0.0, 0.0));
|
||||
}
|
||||
for (ModelObject* mo : model.objects) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
}
|
||||
print.apply(model, config);
|
||||
}
|
||||
|
||||
// Build cubes and run validate(), collecting warnings; returns the blocking error.
|
||||
StringObjectException validate_cubes(const DynamicPrintConfig& config,
|
||||
std::vector<StringObjectException>& warnings,
|
||||
int n = 1, bool overlap = false)
|
||||
{
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
build_cubes(model, print, config, n, overlap);
|
||||
return print.validate(&warnings);
|
||||
}
|
||||
|
||||
size_t count_opt_key(const std::vector<StringObjectException>& warnings, const std::string& key)
|
||||
{
|
||||
return std::count_if(warnings.begin(), warnings.end(),
|
||||
[&](const StringObjectException& w) { return w.opt_key == key; });
|
||||
}
|
||||
|
||||
// Make `default_acceleration` exceed the machine's extruding-acceleration limit.
|
||||
void trigger_acceleration_warning(DynamicPrintConfig& c)
|
||||
{
|
||||
c.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats{ 100. });
|
||||
c.set_key_value("default_acceleration", new ConfigOptionFloat(100000.));
|
||||
}
|
||||
|
||||
// Make `default_jerk` exceed the machine's jerk limit (junction deviation off so
|
||||
// the jerk check is not skipped).
|
||||
void trigger_jerk_warning(DynamicPrintConfig& c)
|
||||
{
|
||||
c.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats{ 0. });
|
||||
c.set_key_value("machine_max_jerk_x", new ConfigOptionFloats{ 1. });
|
||||
c.set_key_value("machine_max_jerk_y", new ConfigOptionFloats{ 1. });
|
||||
c.set_key_value("default_jerk", new ConfigOptionFloat(9999.));
|
||||
}
|
||||
|
||||
// Precise outer wall is ignored unless the wall sequence is inner-outer.
|
||||
void trigger_precise_wall_warning(DynamicPrintConfig& c)
|
||||
{
|
||||
c.set_key_value("precise_outer_wall", new ConfigOptionBool(true));
|
||||
c.set_key_value("wall_sequence", new ConfigOptionEnum<WallSequence>(WallSequence::OuterInner));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Print::validate stacks independent warnings", "[Print][validate]")
|
||||
{
|
||||
// Two unrelated checks (region precise-wall + machine acceleration) must each
|
||||
// contribute their own entry.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
trigger_precise_wall_warning(config);
|
||||
trigger_acceleration_warning(config);
|
||||
|
||||
std::vector<StringObjectException> warnings;
|
||||
StringObjectException err = validate_cubes(config, warnings);
|
||||
|
||||
CHECK(err.string.empty());
|
||||
CHECK(warnings.size() >= 2);
|
||||
CHECK(count_opt_key(warnings, "precise_outer_wall") == 1); // jump-to key is preserved
|
||||
for (const auto& w : warnings)
|
||||
CHECK(w.is_warning); // every collected entry is a warning
|
||||
}
|
||||
|
||||
TEST_CASE("Print::validate coalesces motion-ability warnings into one", "[Print][validate]")
|
||||
{
|
||||
// The jerk/junction/acceleration checks are mutually exclusive (gated on a shared
|
||||
// key), so adding a second motion trigger must NOT add a second warning.
|
||||
DynamicPrintConfig accel_only = DynamicPrintConfig::full_print_config();
|
||||
trigger_acceleration_warning(accel_only);
|
||||
std::vector<StringObjectException> w_accel;
|
||||
CHECK(validate_cubes(accel_only, w_accel).string.empty());
|
||||
|
||||
DynamicPrintConfig accel_and_jerk = DynamicPrintConfig::full_print_config();
|
||||
trigger_acceleration_warning(accel_and_jerk);
|
||||
trigger_jerk_warning(accel_and_jerk);
|
||||
std::vector<StringObjectException> w_both;
|
||||
CHECK(validate_cubes(accel_and_jerk, w_both).string.empty());
|
||||
|
||||
CHECK(w_accel.size() >= 1);
|
||||
CHECK(w_both.size() == w_accel.size()); // the extra motion trigger collapses into the same warning
|
||||
}
|
||||
|
||||
TEST_CASE("Print::validate reports the clumping-detection warning", "[Print][validate]")
|
||||
{
|
||||
// A distinct single-shot path: clumping/wrapping detection without a prime tower warns
|
||||
// (and carries the enable_prime_tower jump-to key). enable_prime_tower must be off, as
|
||||
// the warning lives in the no-prime-tower branch.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_key_value("enable_prime_tower", new ConfigOptionBool(false));
|
||||
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
|
||||
|
||||
std::vector<StringObjectException> warnings;
|
||||
StringObjectException err = validate_cubes(config, warnings);
|
||||
|
||||
CHECK(err.string.empty());
|
||||
CHECK(count_opt_key(warnings, "enable_prime_tower") == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("Print::validate concatenates layered-clearance collisions into one warning", "[Print][validate]")
|
||||
{
|
||||
// In by-layer mode, layered_print_cleareance_valid folds every too-close pair into a
|
||||
// single warning entry (newline-joined), unlike the per-check stacking above. Isolate
|
||||
// that entry by type so unrelated default-config warnings don't affect the assertion.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
std::vector<StringObjectException> warnings;
|
||||
StringObjectException err = validate_cubes(config, warnings, /*n=*/3, /*overlap=*/true);
|
||||
|
||||
CHECK(err.string.empty());
|
||||
auto is_layered = [](const StringObjectException& w) {
|
||||
return w.type == STRING_EXCEPT_OBJECT_COLLISION_IN_LAYER_PRINT; };
|
||||
REQUIRE(std::count_if(warnings.begin(), warnings.end(), is_layered) == 1); // 3 objects, 2 collisions, 1 entry
|
||||
auto it = std::find_if(warnings.begin(), warnings.end(), is_layered);
|
||||
CHECK(it->string.find('\n') != std::string::npos); // the collisions were concatenated
|
||||
}
|
||||
|
||||
TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate]")
|
||||
{
|
||||
// Callers may pass no warnings sink: a warning-producing config must not crash
|
||||
// and must still return without a blocking error.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
trigger_precise_wall_warning(config);
|
||||
trigger_acceleration_warning(config);
|
||||
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
build_cubes(model, print, config, /*n=*/1, /*overlap=*/false);
|
||||
|
||||
StringObjectException err = print.validate(); // warnings == nullptr
|
||||
CHECK(err.string.empty());
|
||||
}
|
||||
|
||||
@@ -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,26 @@ 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][.]") {
|
||||
// [NotWorking]: slice() intermittently throws clipper's "Coordinate outside allowed
|
||||
// range" in CI (Linux) while passing locally. Disabled pending a root-cause fix in a
|
||||
// follow-up PR.
|
||||
SCENARIO( "PrintGCode basic functionality", "[PrintGCode][NotWorking]") {
|
||||
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 +67,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 +84,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 +121,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 +137,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 +163,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 +180,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 +199,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 +225,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));
|
||||
}
|
||||
|
||||
@@ -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") {
|
||||
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, {
|
||||
{ "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;
|
||||
}
|
||||
{ "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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,23 +3,26 @@
|
||||
#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;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
|
||||
{
|
||||
// if the command is a T command, set the the current tool
|
||||
// if the command is a T command, set the current tool
|
||||
if (boost::starts_with(line.cmd(), "T")) {
|
||||
tool = atoi(line.cmd().data() + 1);
|
||||
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) {
|
||||
@@ -29,16 +32,16 @@ static int get_brim_tool(const std::string &gcode)
|
||||
return brim_tool;
|
||||
}
|
||||
|
||||
TEST_CASE("Skirt height is honored", "[Skirt][.]") {
|
||||
// [NotWorking]: slice() intermittently throws clipper's "Coordinate outside allowed
|
||||
// range" in CI (Linux) while passing locally. Disabled pending a root-cause fix in a
|
||||
// follow-up PR.
|
||||
TEST_CASE("Skirt height is honored", "[SkirtBrim][NotWorking]") {
|
||||
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 +52,39 @@ 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][.]") {
|
||||
// [NotWorking]: see "Skirt height is honored" above; same CI-only clipper range throw.
|
||||
SCENARIO("Skirt and brim generation", "[SkirtBrim][NotWorking]") {
|
||||
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 +121,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 +171,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
|
||||
}
|
||||
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");
|
||||
|
||||
#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)) {
|
||||
}
|
||||
}
|
||||
// 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)));
|
||||
});
|
||||
Slic3r::Polygon convex_hull = Slic3r::Geometry::convex_hull(extrusion_points);
|
||||
double hull_perimeter = unscale<double>(convex_hull.split_at_first_point().length());
|
||||
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
WHEN("Large minimum skirt length is used.") {
|
||||
config.set("min_skirt_length", 20);
|
||||
THEN("Gcode generation doesn't crash") {
|
||||
REQUIRE(! Slic3r::Test::slice({TestMesh::cube_20x20x20}, config).empty());
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests_main.cpp
|
||||
${_TEST_NAME}_tests.cpp
|
||||
test_geometry.cpp
|
||||
test_nfp.cpp
|
||||
test_nfp_placer.cpp
|
||||
printer_parts.cpp
|
||||
printer_parts.hpp
|
||||
libnest2d_test_utils.hpp
|
||||
)
|
||||
|
||||
target_link_libraries(${_TEST_NAME}_tests test_common libnest2d Catch2::Catch2WithMain)
|
||||
@@ -10,4 +14,4 @@ set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
// Shared setup for the libnest2d test suite.
|
||||
//
|
||||
// The no-fit-polygon numeric backend specialised below changes how NFP is
|
||||
// computed for the whole program, so every translation unit that instantiates
|
||||
// NFP (the geometry, nfp and placer tests) must see the same definition. Keep
|
||||
// it here and include this header from every libnest2d test file.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <libnest2d/libnest2d.hpp>
|
||||
#include <libnest2d/utils/rotcalipers.hpp>
|
||||
|
||||
#if defined(_MSC_VER) && defined(__clang__)
|
||||
#define BOOST_NO_CXX17_HDR_STRING_VIEW
|
||||
#endif
|
||||
|
||||
#include "boost/multiprecision/integer.hpp"
|
||||
#include "boost/rational.hpp"
|
||||
|
||||
namespace libnest2d {
|
||||
|
||||
#if !defined(_MSC_VER) && defined(__SIZEOF_INT128__) && !defined(__APPLE__)
|
||||
using LargeInt = __int128;
|
||||
#else
|
||||
using LargeInt = boost::multiprecision::int128_t;
|
||||
template<> struct _NumTag<LargeInt> { using Type = ScalarTag; };
|
||||
#endif
|
||||
template<class T> struct _NumTag<boost::rational<T>> { using Type = RationalTag; };
|
||||
|
||||
using RectangleItem = libnest2d::Rectangle;
|
||||
|
||||
namespace nfp {
|
||||
|
||||
// Use exact rational arithmetic for the convex NFP so the tests are not at the
|
||||
// mercy of floating-point rounding.
|
||||
template<class S>
|
||||
struct NfpImpl<S, NfpLevel::CONVEX_ONLY> {
|
||||
NfpResult<S> operator()(const S &sh, const S &other) {
|
||||
return nfpConvexOnly<S, boost::rational<LargeInt>>(sh, other);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace nfp
|
||||
} // namespace libnest2d
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libnest2d_test_utils.hpp"
|
||||
|
||||
using namespace libnest2d;
|
||||
|
||||
// Basic behaviour of the Item type and the high-level nest() entry point:
|
||||
// items copy independently, and nest() leaves degenerate or oversized items
|
||||
// untouched.
|
||||
|
||||
TEST_CASE("Item construction and copy", "[Nesting]") {
|
||||
Item sh = { {0, 0}, {1, 0}, {1, 1}, {0, 1} };
|
||||
REQUIRE(sh.vertexCount() == 4u);
|
||||
|
||||
Item sh2({ {0, 0}, {1, 0}, {1, 1}, {0, 1} });
|
||||
REQUIRE(sh2.vertexCount() == 4u);
|
||||
|
||||
Item sh3 = sh2; // copy
|
||||
REQUIRE(sh3.vertexCount() == 4u);
|
||||
|
||||
sh2 = {}; // clearing the original leaves the copy intact
|
||||
REQUIRE(sh2.vertexCount() == 0u);
|
||||
REQUIRE(sh3.vertexCount() == 4u);
|
||||
}
|
||||
|
||||
TEST_CASE("nest() leaves an empty or zero-area item untouched", "[Nesting]") {
|
||||
auto bin = Box(250000000, 210000000);
|
||||
|
||||
std::vector<Item> items;
|
||||
items.emplace_back(Item{}); // empty item
|
||||
items.emplace_back(Item{ {0, 200} }); // zero-area item
|
||||
|
||||
size_t bins = nest(items, bin);
|
||||
|
||||
REQUIRE(bins == 0u);
|
||||
for (const auto &itm : items) REQUIRE(itm.binId() == BIN_ID_UNSET);
|
||||
}
|
||||
|
||||
TEST_CASE("nest() leaves an item larger than the bin untouched", "[Nesting]") {
|
||||
auto bin = Box(250000000, 210000000);
|
||||
|
||||
std::vector<Item> items;
|
||||
items.emplace_back(RectangleItem{250000001, 210000001}); // larger than the bin
|
||||
|
||||
size_t bins = nest(items, bin);
|
||||
|
||||
REQUIRE(bins == 0u);
|
||||
REQUIRE(items.front().binId() == BIN_ID_UNSET);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libnest2d_test_utils.hpp"
|
||||
#include "printer_parts.hpp"
|
||||
|
||||
using namespace libnest2d;
|
||||
|
||||
namespace {
|
||||
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
// Geometry values round-trip through floating point, so compare with a small
|
||||
// tolerance that works both near and away from zero.
|
||||
void require_close(double value, double expected) {
|
||||
REQUIRE_THAT(value, WithinRel(expected, 1e-9) || WithinAbs(expected, 1e-9));
|
||||
}
|
||||
|
||||
// The printer parts as nestable items, computed once.
|
||||
const std::vector<Item> &prusa_parts() {
|
||||
static const std::vector<Item> parts = [] {
|
||||
std::vector<Item> ret;
|
||||
ret.reserve(PRINTER_PART_POLYGONS.size());
|
||||
for (auto &inp : PRINTER_PART_POLYGONS) {
|
||||
auto inp_cpy = inp;
|
||||
if (ClosureTypeV<PathImpl> == Closure::OPEN)
|
||||
inp_cpy.points.pop_back();
|
||||
if constexpr (!is_clockwise<PathImpl>())
|
||||
std::reverse(inp_cpy.begin(), inp_cpy.end());
|
||||
ret.emplace_back(inp_cpy);
|
||||
}
|
||||
return ret;
|
||||
}();
|
||||
return parts;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Degree and radian conversion round-trips", "[Geometry]") {
|
||||
Degrees deg(180);
|
||||
Radians rad(deg);
|
||||
|
||||
require_close(rad, Pi);
|
||||
require_close(deg, 180);
|
||||
require_close(Degrees(rad), 180);
|
||||
require_close(rad, Radians(deg));
|
||||
require_close(Degrees(rad), deg);
|
||||
REQUIRE(rad == deg);
|
||||
}
|
||||
|
||||
TEST_CASE("Segment angle to the X axis", "[Geometry]") {
|
||||
auto quadrant = [](Point to) { return Degrees(Segment({0, 0}, to).angleToXaxis()); };
|
||||
|
||||
REQUIRE(quadrant({12, -10}) > 270); REQUIRE(quadrant({12, -10}) < 360);
|
||||
REQUIRE(quadrant({12, 10}) > 0); REQUIRE(quadrant({12, 10}) < 90);
|
||||
REQUIRE(quadrant({-12, 10}) > 90); REQUIRE(quadrant({-12, 10}) < 180);
|
||||
REQUIRE(quadrant({-12, -10}) > 180); REQUIRE(quadrant({-12, -10}) < 270);
|
||||
|
||||
require_close(quadrant({1, 0}), 0);
|
||||
require_close(quadrant({0, 1}), 90);
|
||||
require_close(quadrant({-1, 0}), 180);
|
||||
require_close(quadrant({0, -1}), 270);
|
||||
}
|
||||
|
||||
TEST_CASE("Point to segment distance", "[Geometry]") {
|
||||
Point p2 = {10, 0};
|
||||
Segment seg({0, 0}, {10, 10});
|
||||
|
||||
auto check = [](TCompute<Coord> val, TCompute<Coord> expected) {
|
||||
if (std::is_floating_point<TCompute<Coord>>::value)
|
||||
require_close(double(val), double(expected));
|
||||
else
|
||||
REQUIRE(val == expected);
|
||||
};
|
||||
|
||||
auto h = pointlike::horizontalDistance(p2, seg);
|
||||
REQUIRE(h.second);
|
||||
check(h.first, 10);
|
||||
|
||||
auto v = pointlike::verticalDistance(p2, seg);
|
||||
REQUIRE(v.second);
|
||||
check(v.first, -10);
|
||||
|
||||
v = pointlike::verticalDistance(Point{10, 20}, seg);
|
||||
REQUIRE(v.second);
|
||||
check(v.first, 10);
|
||||
|
||||
Point p4 = {80, 0};
|
||||
Segment seg2({0, 0}, {0, 40});
|
||||
|
||||
h = pointlike::horizontalDistance(p4, seg2);
|
||||
REQUIRE(h.second);
|
||||
check(h.first, 80);
|
||||
|
||||
v = pointlike::verticalDistance(p4, seg2);
|
||||
REQUIRE_FALSE(v.second); // the point does not project onto the segment
|
||||
}
|
||||
|
||||
TEST_CASE("Item area", "[Geometry]") {
|
||||
require_close(RectangleItem(10, 10).area(), 100);
|
||||
require_close(RectangleItem(100, 100).area(), 10000);
|
||||
|
||||
Item item = {
|
||||
{61, 97}, {70, 151}, {176, 151}, {189, 138},
|
||||
{189, 59}, {70, 59}, {61, 77}, {61, 97}
|
||||
};
|
||||
REQUIRE(std::abs(shapelike::area(item.transformedShape())) > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Point inside polygon", "[Geometry]") {
|
||||
RectangleItem rect(10, 10);
|
||||
|
||||
REQUIRE(rect.isInside(Point{1, 1}));
|
||||
REQUIRE(rect.isInside(Point{3, 3}));
|
||||
REQUIRE_FALSE(rect.isInside(Point{11, 11}));
|
||||
REQUIRE_FALSE(rect.isInside(Point{11, 12}));
|
||||
}
|
||||
|
||||
TEST_CASE("Bounding circle of the printer parts", "[Geometry]") {
|
||||
PolygonImpl p = {{{0, 10}, {10, 0}, {0, -10}, {0, 10}}, {}};
|
||||
Circle c = placers::boundingCircle(p);
|
||||
|
||||
require_close(getX(c.center()), 0);
|
||||
require_close(getY(c.center()), 0);
|
||||
require_close(c.radius(), 10);
|
||||
|
||||
shapelike::translate(p, PointImpl{10, 10});
|
||||
c = placers::boundingCircle(p);
|
||||
require_close(getX(c.center()), 10);
|
||||
require_close(getY(c.center()), 10);
|
||||
require_close(c.radius(), 10);
|
||||
|
||||
for (auto &part : prusa_parts()) {
|
||||
c = placers::boundingCircle(part.transformedShape());
|
||||
REQUIRE_FALSE(std::isnan(c.radius()));
|
||||
for (auto v : shapelike::contour(part.transformedShape())) {
|
||||
auto d = pointlike::distance(v, c.center());
|
||||
if (d > c.radius())
|
||||
REQUIRE(std::abs(1.0 - d / c.radius()) <= 1e-3); // on the circle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Convex hull of a printer part", "[Geometry]") {
|
||||
PathImpl poly = PRINTER_PART_POLYGONS[0];
|
||||
auto chull = sl::convexHull(poly);
|
||||
|
||||
REQUIRE(chull.size() == poly.size()); // the part is already convex
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
using Unit = int64_t;
|
||||
using Ratio = boost::rational<boost::multiprecision::int128_t>;
|
||||
|
||||
// Reference minimum-area bounding box, found by brute force over every edge
|
||||
// direction, to validate the rotating-calipers implementation.
|
||||
long double ref_min_area_box(const PolygonImpl &p) {
|
||||
long double min_area = std::numeric_limits<long double>::max();
|
||||
|
||||
auto update_min = [&](const Point &a, const Point &b) {
|
||||
PolygonImpl rotated = p;
|
||||
sl::rotate(rotated, -Segment(a, b).angleToXaxis());
|
||||
min_area = std::min(min_area, cast<long double>(sl::area(sl::boundingBox(rotated))));
|
||||
};
|
||||
|
||||
auto it = sl::cbegin(p), itx = std::next(it);
|
||||
while (itx != sl::cend(p)) { update_min(*it, *itx); ++it; ++itx; }
|
||||
update_min(*std::prev(sl::cend(p)), *sl::cbegin(p));
|
||||
|
||||
return min_area;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Minimum-area bounding box via rotating calipers", "[Geometry]") {
|
||||
const long double tolerance = 500e6l;
|
||||
|
||||
for (const PathImpl &part : PRINTER_PART_POLYGONS) {
|
||||
auto area = cast<long double>(minAreaBoundingBox<PathImpl, Unit, Ratio>(part).area());
|
||||
REQUIRE(std::abs(ref_min_area_box(PolygonImpl(part)) - area) < tolerance);
|
||||
}
|
||||
|
||||
for (PathImpl part : STEGOSAUR_POLYGONS) {
|
||||
std::reverse(part.begin(), part.end());
|
||||
PolygonImpl poly(removeCollinearPoints<PathImpl, PointImpl, Unit>(part, 1000000));
|
||||
auto area = cast<long double>(minAreaBoundingBox<PolygonImpl, Unit, Ratio>(poly).area());
|
||||
REQUIRE(std::abs(ref_min_area_box(poly) - area) < tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libnest2d_test_utils.hpp"
|
||||
|
||||
using namespace libnest2d;
|
||||
|
||||
namespace {
|
||||
|
||||
struct ItemPair {
|
||||
Item orbiter;
|
||||
Item stationary;
|
||||
};
|
||||
|
||||
std::vector<ItemPair> nfp_testdata = {
|
||||
{
|
||||
{
|
||||
{80, 50},
|
||||
{100, 70},
|
||||
{120, 50}
|
||||
},
|
||||
{
|
||||
{10, 10},
|
||||
{10, 40},
|
||||
{40, 40},
|
||||
{40, 10}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
{80, 50},
|
||||
{60, 70},
|
||||
{80, 90},
|
||||
{120, 90},
|
||||
{140, 70},
|
||||
{120, 50}
|
||||
},
|
||||
{
|
||||
{10, 10},
|
||||
{10, 40},
|
||||
{40, 40},
|
||||
{40, 10}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
{40, 10},
|
||||
{30, 10},
|
||||
{20, 20},
|
||||
{20, 30},
|
||||
{30, 40},
|
||||
{40, 40},
|
||||
{50, 30},
|
||||
{50, 20}
|
||||
},
|
||||
{
|
||||
{80, 0},
|
||||
{80, 30},
|
||||
{110, 30},
|
||||
{110, 0}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
{117, 107},
|
||||
{118, 109},
|
||||
{120, 112},
|
||||
{122, 113},
|
||||
{128, 113},
|
||||
{130, 112},
|
||||
{132, 109},
|
||||
{133, 107},
|
||||
{133, 103},
|
||||
{132, 101},
|
||||
{130, 98},
|
||||
{128, 97},
|
||||
{122, 97},
|
||||
{120, 98},
|
||||
{118, 101},
|
||||
{117, 103}
|
||||
},
|
||||
{
|
||||
{102, 116},
|
||||
{111, 126},
|
||||
{114, 126},
|
||||
{144, 106},
|
||||
{148, 100},
|
||||
{148, 85},
|
||||
{147, 84},
|
||||
{102, 84}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
{99, 122},
|
||||
{108, 140},
|
||||
{110, 142},
|
||||
{139, 142},
|
||||
{151, 122},
|
||||
{151, 102},
|
||||
{142, 70},
|
||||
{139, 68},
|
||||
{111, 68},
|
||||
{108, 70},
|
||||
{99, 102}
|
||||
},
|
||||
{
|
||||
{107, 124},
|
||||
{128, 125},
|
||||
{133, 125},
|
||||
{136, 124},
|
||||
{140, 121},
|
||||
{142, 119},
|
||||
{143, 116},
|
||||
{143, 109},
|
||||
{141, 93},
|
||||
{139, 89},
|
||||
{136, 86},
|
||||
{134, 85},
|
||||
{108, 85},
|
||||
{107, 86}
|
||||
}
|
||||
},
|
||||
{
|
||||
{
|
||||
{91, 100},
|
||||
{94, 144},
|
||||
{117, 153},
|
||||
{118, 153},
|
||||
{159, 112},
|
||||
{159, 110},
|
||||
{156, 66},
|
||||
{133, 57},
|
||||
{132, 57},
|
||||
{91, 98}
|
||||
},
|
||||
{
|
||||
{101, 90},
|
||||
{103, 98},
|
||||
{107, 113},
|
||||
{114, 125},
|
||||
{115, 126},
|
||||
{135, 126},
|
||||
{136, 125},
|
||||
{144, 114},
|
||||
{149, 90},
|
||||
{149, 89},
|
||||
{148, 87},
|
||||
{145, 84},
|
||||
{105, 84},
|
||||
{102, 87},
|
||||
{101, 89}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// libnest2d's vertex order depends on the backend; normalise to clockwise.
|
||||
Item reversed_if_ccw(Item it) {
|
||||
if (!is_clockwise<PolygonImpl>()) {
|
||||
auto raw = it.rawShape();
|
||||
std::reverse(sl::begin(raw), sl::end(raw));
|
||||
it = Item{raw};
|
||||
}
|
||||
return it;
|
||||
}
|
||||
|
||||
// Sliding `orbiter` around `stationary` along their no-fit polygon must keep the
|
||||
// two shapes touching at every NFP vertex, and `stationary` must lie inside the
|
||||
// resulting inner-fit polygon.
|
||||
template<nfp::NfpLevel lvl, Coord SCALE>
|
||||
void check_nfp(const std::vector<ItemPair> &testdata) {
|
||||
auto check_pair = [](Item orbiter, Item stationary) {
|
||||
orbiter.translate({210 * SCALE, 0});
|
||||
|
||||
auto &&nfp = nfp::noFitPolygon<lvl>(stationary.rawShape(), orbiter.transformedShape());
|
||||
placers::correctNfpPosition(nfp, stationary, orbiter);
|
||||
REQUIRE(shapelike::isValid(nfp.first).first);
|
||||
|
||||
Item infp(nfp.first);
|
||||
REQUIRE(stationary.isInside(infp));
|
||||
|
||||
auto vo = nfp::referenceVertex(orbiter.transformedShape());
|
||||
for (auto v : infp) {
|
||||
Item moved = orbiter;
|
||||
moved.translate({getX(v) - getX(vo), getY(v) - getY(vo)});
|
||||
REQUIRE(Item::touches(moved, stationary));
|
||||
}
|
||||
};
|
||||
|
||||
for (const ItemPair &td : testdata) {
|
||||
check_pair(reversed_if_ccw(td.orbiter), reversed_if_ccw(td.stationary));
|
||||
check_pair(reversed_if_ccw(td.stationary), reversed_if_ccw(td.orbiter));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("No-fit polygon of convex shapes keeps the items touching", "[Geometry][NFP]") {
|
||||
check_nfp<nfp::NfpLevel::CONVEX_ONLY, 1>(nfp_testdata);
|
||||
}
|
||||
|
||||
TEST_CASE("BottomLeftPlacer left and down polygons", "[Geometry][NFP]") {
|
||||
Box bin(100, 100);
|
||||
BottomLeftPlacer placer(bin);
|
||||
|
||||
PathImpl pitem = {{70, 75}, {88, 60}, {65, 50}, {60, 30}, {80, 20},
|
||||
{42, 20}, {35, 35}, {35, 55}, {40, 75}};
|
||||
PathImpl left_control = {{40, 75}, {35, 55}, {35, 35}, {42, 20}, {0, 20}, {0, 75}};
|
||||
PathImpl down_control = {{88, 60}, {88, 0}, {35, 0}, {35, 35},
|
||||
{42, 20}, {80, 20}, {60, 30}, {65, 50}};
|
||||
|
||||
if constexpr (!is_clockwise<PathImpl>()) {
|
||||
std::reverse(sl::begin(pitem), sl::end(pitem));
|
||||
std::reverse(sl::begin(left_control), sl::end(left_control));
|
||||
std::reverse(sl::begin(down_control), sl::end(down_control));
|
||||
}
|
||||
if constexpr (ClosureTypeV<PathImpl> == Closure::CLOSED) {
|
||||
sl::addVertex(pitem, sl::front(pitem));
|
||||
sl::addVertex(left_control, sl::front(left_control));
|
||||
sl::addVertex(down_control, sl::front(down_control));
|
||||
}
|
||||
|
||||
auto require_same_vertices = [](const Item &got, const Item &expected) {
|
||||
REQUIRE(shapelike::isValid(got.rawShape()).first);
|
||||
REQUIRE(got.vertexCount() == expected.vertexCount());
|
||||
for (unsigned long i = 0; i < expected.vertexCount(); ++i) {
|
||||
REQUIRE(getX(got.vertex(i)) == getX(expected.vertex(i)));
|
||||
REQUIRE(getY(got.vertex(i)) == getY(expected.vertex(i)));
|
||||
}
|
||||
};
|
||||
|
||||
Item item{pitem};
|
||||
require_same_vertices(Item(placer.leftPoly(item)), Item{left_control});
|
||||
require_same_vertices(Item(placer.downPoly(item)), Item{down_control});
|
||||
}
|
||||
|
||||
TEST_CASE("EdgeCache maps a parameter to a contour point", "[Geometry][NFP]") {
|
||||
RectangleItem input(10, 10);
|
||||
placers::EdgeCache<PolygonImpl> ecache(input);
|
||||
|
||||
auto first = *input.begin();
|
||||
REQUIRE(getX(first) == getX(ecache.coords(0)));
|
||||
REQUIRE(getY(first) == getY(ecache.coords(0)));
|
||||
|
||||
auto last = *std::prev(input.end());
|
||||
REQUIRE(getX(last) == getX(ecache.coords(1.0)));
|
||||
REQUIRE(getY(last) == getY(ecache.coords(1.0)));
|
||||
|
||||
for (int i = 0; i <= 100; ++i)
|
||||
REQUIRE(shapelike::touches(ecache.coords(i * 0.01), input.transformedShape()));
|
||||
}
|
||||
|
||||
TEST_CASE("Merging a pile with a polygon", "[Geometry][NFP]") {
|
||||
RectangleItem rect1(10, 15), rect2(15, 15), rect3(20, 15);
|
||||
rect2.translate({10, 0});
|
||||
rect3.translate({25, 0});
|
||||
|
||||
TMultiShape<PolygonImpl> pile;
|
||||
pile.push_back(rect1.transformedShape());
|
||||
pile.push_back(rect2.transformedShape());
|
||||
|
||||
auto result = nfp::merge(pile, rect3.transformedShape());
|
||||
REQUIRE(result.size() == 1); // the three abutting rectangles merge into one
|
||||
|
||||
RectangleItem ref(45, 15);
|
||||
REQUIRE_THAT(shapelike::area(result.front()),
|
||||
Catch::Matchers::WithinRel(ref.area(), 1e-9));
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libnest2d_test_utils.hpp"
|
||||
|
||||
using namespace libnest2d;
|
||||
|
||||
// NfpPlacer is the No-Fit-Polygon placement engine that Orca's arranger drives
|
||||
// (via _Nester/FirstFitSelection in Arrange.cpp). These exercise the placer
|
||||
// directly: pack()/accept() are the core geometric placement primitives.
|
||||
namespace {
|
||||
|
||||
struct NfpPlacerFixture {
|
||||
using Cfg = NfpPlacer::Config;
|
||||
Box bin{250000000, 210000000}; // 250 x 210 mm bed at 1e6 scale
|
||||
|
||||
NfpPlacer placer_with(Cfg cfg = {}) const {
|
||||
cfg.parallel = false; // deterministic, single-threaded for tests
|
||||
NfpPlacer p{bin};
|
||||
p.configure(cfg);
|
||||
return p;
|
||||
}
|
||||
|
||||
// pack + accept; returns whether the item was placed.
|
||||
static bool place(NfpPlacer &p, Item &item) {
|
||||
auto res = p.pack(item);
|
||||
if (res) p.accept(res);
|
||||
return bool(res);
|
||||
}
|
||||
|
||||
// Place every item and REQUIRE each one is packed.
|
||||
static void place_all(NfpPlacer &p, std::vector<RectangleItem> &items) {
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
INFO("packing item " << i);
|
||||
REQUIRE(place(p, items[i]));
|
||||
}
|
||||
}
|
||||
|
||||
// No two items overlap (a shared edge is allowed) and each stays in the bin.
|
||||
void require_disjoint_in_bin(std::vector<RectangleItem> &items) const {
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
REQUIRE(sl::isInside(items[i].boundingBox(), bin));
|
||||
for (size_t j = i + 1; j < items.size(); ++j) {
|
||||
const bool overlaps = Item::intersects(items[i], items[j]) &&
|
||||
!Item::touches(items[i], items[j]);
|
||||
INFO("items " << i << " and " << j);
|
||||
REQUIRE_FALSE(overlaps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::vector<RectangleItem> squares(size_t n, Coord side) {
|
||||
return std::vector<RectangleItem>(n, RectangleItem{side, side});
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer places a single item inside the bin", "[Nesting][Placer]") {
|
||||
NfpPlacer placer = placer_with();
|
||||
RectangleItem item{100000000, 100000000};
|
||||
|
||||
REQUIRE(place(placer, item));
|
||||
REQUIRE(placer.getItems().size() == 1u);
|
||||
REQUIRE(sl::isInside(item.boundingBox(), bin));
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer rejects an item larger than the bin", "[Nesting][Placer]") {
|
||||
NfpPlacer placer = placer_with();
|
||||
RectangleItem big{300000000, 300000000}; // wider and taller than the bin
|
||||
|
||||
auto res = placer.pack(big);
|
||||
REQUIRE_FALSE(bool(res));
|
||||
REQUIRE(placer.getItems().empty());
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer positions the first item for any starting point", "[Nesting][Placer]") {
|
||||
// setInitialPosition() seeds the first item from the configured starting
|
||||
// corner; pack() (without accept()) drives that switch for every value.
|
||||
using A = Cfg::Alignment;
|
||||
auto start = GENERATE(A::CENTER, A::BOTTOM_LEFT, A::BOTTOM_RIGHT,
|
||||
A::TOP_LEFT, A::TOP_RIGHT, A::USER_DEFINED, A::DONT_ALIGN);
|
||||
CAPTURE(int(start));
|
||||
|
||||
Cfg cfg;
|
||||
cfg.starting_point = start;
|
||||
cfg.best_object_pos = bin.center();
|
||||
NfpPlacer placer = placer_with(cfg);
|
||||
|
||||
RectangleItem item{100000000, 100000000};
|
||||
auto res = placer.pack(item);
|
||||
REQUIRE(bool(res));
|
||||
REQUIRE(sl::isInside(item.boundingBox(), bin));
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer packs many items without overlap", "[Nesting][Placer]") {
|
||||
// Each item is placed against the no-fit polygon of the growing pile.
|
||||
auto items = squares(GENERATE(2u, 6u, 9u), 60000000);
|
||||
NfpPlacer placer = placer_with();
|
||||
|
||||
place_all(placer, items);
|
||||
REQUIRE(placer.getItems().size() == items.size());
|
||||
require_disjoint_in_bin(items);
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer evaluates the rotation candidates", "[Nesting][Placer]") {
|
||||
Cfg cfg;
|
||||
cfg.rotations = {0.0, Pi / 2.0}; // exercise the rotation search loop
|
||||
NfpPlacer placer = placer_with(cfg);
|
||||
|
||||
std::vector<RectangleItem> rects = {
|
||||
{180000000, 40000000}, {180000000, 40000000}, {180000000, 40000000}};
|
||||
place_all(placer, rects);
|
||||
require_disjoint_in_bin(rects);
|
||||
}
|
||||
|
||||
TEST_CASE_METHOD(NfpPlacerFixture, "NfpPlacer's final alignment keeps the pile clear of a fixed obstacle", "[Nesting][Placer]") {
|
||||
// A preloaded fixed item makes finalAlign's recentring keep the pile clear of
|
||||
// it instead of dropping it straight onto the bin centre. Box{w,h} centres on
|
||||
// the origin, so the obstacle sits there too; virtual keeps it in place.
|
||||
RectangleItem obstacle{80000000, 80000000};
|
||||
obstacle.translation({-40000000, -40000000}); // 80x80 mm centred in the bin (origin)
|
||||
obstacle.markAsFixedInBin(0);
|
||||
obstacle.is_virt_object = true;
|
||||
|
||||
auto items = squares(4, 30000000);
|
||||
{
|
||||
NfpPlacer placer = placer_with();
|
||||
NfpPlacer::ItemGroup fixed;
|
||||
fixed.emplace_back(obstacle);
|
||||
placer.preload(fixed);
|
||||
place_all(placer, items);
|
||||
} // the placer's destructor runs finalAlign, translating the packed items
|
||||
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
INFO("item " << i);
|
||||
const bool overlaps = Item::intersects(items[i], obstacle) &&
|
||||
!Item::touches(items[i], obstacle);
|
||||
REQUIRE_FALSE(overlaps);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_aabbindirect.cpp
|
||||
test_appconfig.cpp
|
||||
test_arachne_walls.cpp
|
||||
test_arrange.cpp
|
||||
test_bambu_networking.cpp
|
||||
test_clipper_offset.cpp
|
||||
test_clipper_utils.cpp
|
||||
@@ -39,4 +40,4 @@ set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
|
||||
#include "libslic3r/Arachne/WallToolPaths.hpp"
|
||||
#include "libslic3r/Arachne/utils/ExtrusionLine.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategyFactory.hpp"
|
||||
#include "libslic3r/Arachne/BeadingStrategy/BeadingStrategy.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -207,3 +210,58 @@ TEST_CASE("Arachne wall generation - 60% min_bead_width", "[Arachne]") {
|
||||
size_t duplicates = run_arachne_test(60);
|
||||
REQUIRE(duplicates == 0);
|
||||
}
|
||||
|
||||
// Regression test for #14376 ("Fuzzy skin artifacting" — a surface bulge at a fixed height).
|
||||
//
|
||||
// PR #14031 changed WideningBeadingStrategy::compute() to take the thin-wall single-bead
|
||||
// branch whenever thickness < getTransitionThickness(1). That branch emits a single bead at
|
||||
// the full wall thickness and ignores the requested bead_count. When the skeletal graph asks
|
||||
// for 2 beads at a thickness inside the 1<->2 transition band (between the inner wall width and
|
||||
// getTransitionThickness(1)), the request was collapsed into one over-wide bead — an
|
||||
// over-extruded line that shows up as a bulge on curved surfaces at a deterministic height.
|
||||
//
|
||||
// Profile mirrors the reporter's project ("0.20mm Standard @BBL X1C", 0.4mm nozzle):
|
||||
// outer 0.42mm / inner 0.45mm, min_bead_width 85% (0.34mm), 2 walls (max_bead_count 4).
|
||||
// For these numbers wall_split_middle_threshold = 2*0.34/0.42 - 1 = 0.619, so
|
||||
// getTransitionThickness(1) = (1 + 0.619) * 0.42 = 0.68mm. A 0.5mm-thick wall therefore sits
|
||||
// in the transition band: alpha produced 2 beads here, beta collapses it to 1 fat bead.
|
||||
TEST_CASE("Arachne widening keeps two beads in transition band (#14376)", "[Arachne]") {
|
||||
using namespace Slic3r::Arachne;
|
||||
|
||||
// Widths in mm; the scaled coord_t values and the thresholds below are both derived from
|
||||
// these so a width change cannot silently desync the transition-band math.
|
||||
const double outer_mm = 0.42, inner_mm = 0.45, min_bead_mm = 0.34; // min_bead = 85% of 0.4mm nozzle
|
||||
|
||||
const coord_t outer_width = scaled<coord_t>(outer_mm);
|
||||
const coord_t inner_width = scaled<coord_t>(inner_mm);
|
||||
const coord_t min_bead_width = scaled<coord_t>(min_bead_mm);
|
||||
const coord_t min_feature_size = scaled<coord_t>(0.10); // 25% of 0.4mm nozzle
|
||||
const coord_t transition_length = scaled<coord_t>(0.40);
|
||||
const coord_t max_bead_count = 4; // 2 * wall_loops
|
||||
|
||||
// Same derivation as WallToolPaths.cpp.
|
||||
const double split_middle_threshold = std::clamp(2.0 * min_bead_mm / outer_mm - 1.0, 0.01, 0.99);
|
||||
const double add_middle_threshold = std::clamp(min_bead_mm / inner_mm, 0.01, 0.99);
|
||||
|
||||
auto strategy = BeadingStrategyFactory::makeStrategy(
|
||||
outer_width, inner_width, transition_length,
|
||||
/*transitioning_angle*/ float(M_PI / 4.0), /*print_thin_walls*/ true,
|
||||
min_bead_width, min_feature_size,
|
||||
split_middle_threshold, add_middle_threshold,
|
||||
max_bead_count, /*outer_wall_offset*/ 0, /*inward_distributed_center_wall_count*/ 1);
|
||||
|
||||
// A wall thickness inside the 1<->2 bead transition band (inner_width < t < transition).
|
||||
const coord_t thickness = scaled<coord_t>(0.50);
|
||||
REQUIRE(thickness > inner_width);
|
||||
REQUIRE(thickness < strategy->getTransitionThickness(1));
|
||||
|
||||
// When the graph requests 2 beads, the strategy must produce 2 beads — not collapse them
|
||||
// into a single full-thickness (bulge) bead.
|
||||
const BeadingStrategy::Beading beading = strategy->compute(thickness, 2);
|
||||
REQUIRE(beading.bead_widths.size() == 2);
|
||||
|
||||
// And neither bead may be over-wide: a single collapsed bead would be ~0.5mm (the full
|
||||
// thickness), well above the configured wall widths.
|
||||
for (const coord_t w : beading.bead_widths)
|
||||
CHECK(w <= inner_width);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/Arrange.hpp"
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::arrangement;
|
||||
|
||||
namespace {
|
||||
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
// Square of the given (scaled) side, lower-left at the origin. bed_idx starts at
|
||||
// 0 because arrange() seeds the nester's bin from it (see ModelArrange.cpp).
|
||||
ArrangePolygon make_square(coord_t side)
|
||||
{
|
||||
ArrangePolygon ap;
|
||||
Polygon p;
|
||||
p.points = {Point(0, 0), Point(side, 0), Point(side, side), Point(0, side)};
|
||||
ap.poly = ExPolygon(p);
|
||||
ap.bed_idx = 0;
|
||||
return ap;
|
||||
}
|
||||
|
||||
ArrangePolygons squares(int n, double side_mm)
|
||||
{
|
||||
ArrangePolygons items;
|
||||
for (int i = 0; i < n; ++i)
|
||||
items.emplace_back(make_square(scaled(side_mm)));
|
||||
return items;
|
||||
}
|
||||
|
||||
// Bed [0,0]..[w,h] in scaled coordinates.
|
||||
BoundingBox bed(double w_mm, double h_mm)
|
||||
{
|
||||
return BoundingBox(Point(0, 0), Point(scaled(w_mm), scaled(h_mm)));
|
||||
}
|
||||
|
||||
// The default progress callback prints to stdout; silence it.
|
||||
ArrangeParams quiet_params(coord_t min_dist = 0)
|
||||
{
|
||||
ArrangeParams p{min_dist};
|
||||
p.progressind = [](unsigned, std::string) {};
|
||||
return p;
|
||||
}
|
||||
|
||||
ExPolygons placed_shapes(const ArrangePolygons &items)
|
||||
{
|
||||
ExPolygons out;
|
||||
out.reserve(items.size());
|
||||
for (const ArrangePolygon &ap : items)
|
||||
out.emplace_back(ap.transformed_poly());
|
||||
return out;
|
||||
}
|
||||
|
||||
// Area double-counted across the shapes: the sum counts overlaps twice, the
|
||||
// union once, so the difference is the overlapping area (0 when disjoint).
|
||||
double overlap_area(const ExPolygons &shapes)
|
||||
{
|
||||
double sum = 0;
|
||||
for (const ExPolygon &e : shapes)
|
||||
sum += e.area();
|
||||
double uni = 0;
|
||||
for (const ExPolygon &e : union_ex(shapes))
|
||||
uni += e.area();
|
||||
return sum - uni;
|
||||
}
|
||||
|
||||
// Relative tolerance absorbs the area-unit rounding the clipper union introduces.
|
||||
bool disjoint(const ExPolygons &shapes)
|
||||
{
|
||||
double total = 0;
|
||||
for (const ExPolygon &e : shapes)
|
||||
total += e.area();
|
||||
return overlap_area(shapes) <= total * 1e-9;
|
||||
}
|
||||
|
||||
void require_no_overlap(const ArrangePolygons &items)
|
||||
{
|
||||
REQUIRE(disjoint(placed_shapes(items)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Prove the overlap check the other tests rely on actually detects overlap.
|
||||
TEST_CASE("overlap_area detects overlap and ignores touching edges", "[Arrange]")
|
||||
{
|
||||
auto square_at = [](double x_mm) {
|
||||
ArrangePolygon ap = make_square(scaled(20.));
|
||||
ap.translation = Vec2crd(scaled(x_mm), 0);
|
||||
return ap.transformed_poly();
|
||||
};
|
||||
ExPolygon a = square_at(0.);
|
||||
|
||||
SECTION("disjoint shapes are reported disjoint") {
|
||||
REQUIRE(disjoint({a, square_at(30.)}));
|
||||
}
|
||||
SECTION("edge-touching shapes are reported disjoint") {
|
||||
REQUIRE(disjoint({a, square_at(20.)}));
|
||||
}
|
||||
SECTION("overlapping shapes are not, and the area is measured") {
|
||||
REQUIRE_FALSE(disjoint({a, square_at(10.)}));
|
||||
REQUIRE_THAT(overlap_area({a, square_at(10.)}),
|
||||
WithinRel(double(scaled(10.)) * scaled(20.), 1e-9)); // 10x20 mm
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Arrange places every item on the physical bed", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items = squares(5, 20.);
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
||||
|
||||
for (const ArrangePolygon &ap : items)
|
||||
REQUIRE(ap.bed_idx == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Arranged items stay within the bed", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items = squares(6, 30.);
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
||||
|
||||
for (const ArrangePolygon &ap : items) {
|
||||
REQUIRE(ap.bed_idx == 0);
|
||||
REQUIRE(bed(200, 200).contains(ap.transformed_poly().contour.bounding_box()));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Arranged items do not overlap", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items = squares(6, 40.);
|
||||
arrange(items, bed(250, 250), quiet_params(scaled(2.)));
|
||||
|
||||
require_no_overlap(items);
|
||||
}
|
||||
|
||||
TEST_CASE("Arrange spaces items by their inflation", "[Arrange]")
|
||||
{
|
||||
// Per-item inflation is how the arranger enforces clearance (the GUI fills it
|
||||
// from min_obj_distance). Two items inflated 4mm each end up >= 8mm apart.
|
||||
ArrangePolygons items = squares(4, 20.);
|
||||
for (ArrangePolygon &ap : items)
|
||||
ap.inflation = scaled(4.);
|
||||
arrange(items, bed(200, 200), quiet_params());
|
||||
|
||||
// Axis-aligned squares are their own bounding boxes, so the clearance between
|
||||
// a pair is the distance between their boxes (1mm slack for nester rounding).
|
||||
std::vector<BoundingBox> boxes;
|
||||
for (const ExPolygon &e : placed_shapes(items))
|
||||
boxes.push_back(e.contour.bounding_box());
|
||||
|
||||
double min_gap = std::numeric_limits<double>::max();
|
||||
for (size_t i = 0; i < boxes.size(); ++i)
|
||||
for (size_t j = i + 1; j < boxes.size(); ++j) {
|
||||
coord_t sx = std::max<coord_t>(0, std::max(boxes[j].min.x() - boxes[i].max.x(),
|
||||
boxes[i].min.x() - boxes[j].max.x()));
|
||||
coord_t sy = std::max<coord_t>(0, std::max(boxes[j].min.y() - boxes[i].max.y(),
|
||||
boxes[i].min.y() - boxes[j].max.y()));
|
||||
min_gap = std::min(min_gap, std::sqrt(double(sx) * sx + double(sy) * sy));
|
||||
}
|
||||
|
||||
REQUIRE(min_gap >= double(scaled(8.)) - double(scaled(0.5)));
|
||||
}
|
||||
|
||||
TEST_CASE("An item larger than the bed cannot be placed", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items;
|
||||
items.emplace_back(make_square(scaled(20.)));
|
||||
items.emplace_back(make_square(scaled(400.))); // far bigger than the bed
|
||||
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(1.)));
|
||||
|
||||
REQUIRE(items[0].bed_idx == 0);
|
||||
REQUIRE(items[1].bed_idx == UNARRANGED);
|
||||
}
|
||||
|
||||
TEST_CASE("Items overflowing one bed spill onto virtual beds", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items = squares(8, 90.); // eight 90mm squares cannot share a 200x200 bed
|
||||
arrange(items, bed(200, 200), quiet_params(scaled(2.)));
|
||||
|
||||
int max_bed = 0;
|
||||
for (const ArrangePolygon &ap : items) {
|
||||
REQUIRE(ap.bed_idx >= 0); // placed somewhere
|
||||
max_bed = std::max(max_bed, ap.bed_idx);
|
||||
}
|
||||
REQUIRE(max_bed >= 1); // at least one on a virtual bed
|
||||
}
|
||||
|
||||
TEST_CASE("Arrange handles an empty input", "[Arrange]")
|
||||
{
|
||||
ArrangePolygons items;
|
||||
REQUIRE_NOTHROW(arrange(items, bed(200, 200), quiet_params()));
|
||||
REQUIRE(items.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Arrange without final alignment keeps items disjoint", "[Arrange]")
|
||||
{
|
||||
// do_final_align = false selects Alignment::DONT_ALIGN (skips recentering).
|
||||
ArrangePolygons items = squares(6, 40.);
|
||||
ArrangeParams params = quiet_params(scaled(2.));
|
||||
params.do_final_align = false;
|
||||
|
||||
arrange(items, bed(250, 250), params);
|
||||
|
||||
for (const ArrangePolygon &ap : items)
|
||||
REQUIRE(ap.bed_idx == 0);
|
||||
require_no_overlap(items);
|
||||
}
|
||||
|
||||
TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
|
||||
{
|
||||
// align_center != (0.5, 0.5) selects Alignment::USER_DEFINED.
|
||||
ArrangePolygons items = squares(5, 30.);
|
||||
ArrangeParams params = quiet_params(scaled(2.));
|
||||
params.align_center = Vec2d(0.3, 0.7);
|
||||
|
||||
arrange(items, bed(250, 250), params);
|
||||
|
||||
for (const ArrangePolygon &ap : items)
|
||||
REQUIRE(ap.bed_idx == 0);
|
||||
require_no_overlap(items);
|
||||
}
|
||||
@@ -452,6 +452,54 @@ SCENARIO("update_non_diff_values_to_base_config preserves child vectors when chi
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vector sizes",
|
||||
"[Config][Variant]") {
|
||||
// Regression: loading a user printer preset that inherits a non-BBL multi-extruder base and
|
||||
// overrides stride-2 machine limits used to throw in ConfigOptionVector::set_only_diff
|
||||
// ("invalid diff_index size"). The base's machine-limit vectors get length-extended by the
|
||||
// nozzle count while it carries no printer_extruder_variant, so the base length (nozzles*2)
|
||||
// no longer matches variant_index.size()*2. The throw was caught upstream and DELETED the
|
||||
// user's preset file. The merge must instead degrade gracefully.
|
||||
GIVEN("A 4-nozzle parent with stride-2 limits extended to nozzles*2 but no printer_extruder_variant") {
|
||||
Slic3r::DynamicPrintConfig parent;
|
||||
Slic3r::DynamicPrintConfig child;
|
||||
|
||||
parent.set_key_value("nozzle_diameter",
|
||||
new Slic3r::ConfigOptionFloats({0.4, 0.4, 0.4, 0.4}));
|
||||
parent.set_key_value("machine_max_acceleration_x",
|
||||
new Slic3r::ConfigOptionFloats({25000, 25000, 25000, 25000, 25000, 25000, 25000, 25000}));
|
||||
|
||||
// Child user preset declares 4 extruder variants and overrides the machine limit.
|
||||
child.set_key_value("printer_extruder_id",
|
||||
new Slic3r::ConfigOptionInts({1, 2, 3, 4}));
|
||||
child.set_key_value("printer_extruder_variant",
|
||||
new Slic3r::ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard",
|
||||
"Direct Drive Standard", "Direct Drive Standard"}));
|
||||
child.set_key_value("machine_max_acceleration_x",
|
||||
new Slic3r::ConfigOptionFloats({8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000}));
|
||||
|
||||
WHEN("update_diff_values_to_child_config merges the child overrides") {
|
||||
std::string id_name = "printer_extruder_id";
|
||||
std::string var_name = "printer_extruder_variant";
|
||||
|
||||
THEN("it does not throw on the legacy size mismatch") {
|
||||
REQUIRE_NOTHROW(parent.update_diff_values_to_child_config(
|
||||
child, id_name, var_name,
|
||||
Slic3r::printer_options_with_variant_1,
|
||||
Slic3r::printer_options_with_variant_2));
|
||||
|
||||
AND_THEN("the child's overridden machine limit is preserved") {
|
||||
auto* mx = parent.option<Slic3r::ConfigOptionFloats>("machine_max_acceleration_x");
|
||||
REQUIRE(mx != nullptr);
|
||||
REQUIRE(mx->values.size() >= 2);
|
||||
REQUIRE_THAT(mx->values[0], Catch::Matchers::WithinAbs(8000.0, 1e-6));
|
||||
REQUIRE_THAT(mx->values[1], Catch::Matchers::WithinAbs(8000.0, 1e-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") {
|
||||
// WHEN("DynamicPrintConfig is serialized and deserialized") {
|
||||
// auto now = std::chrono::high_resolution_clock::now();
|
||||
|
||||
@@ -9,4 +9,4 @@ set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
@@ -12,4 +12,4 @@ set_property(TARGET ${_TEST_NAME}_tests PROPERTY FOLDER "tests")
|
||||
|
||||
orcaslicer_copy_test_dlls()
|
||||
|
||||
catch_discover_tests(${_TEST_NAME}_tests)
|
||||
orcaslicer_discover_tests(${_TEST_NAME}_tests)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user