fff_print: a maintainable testing framework (proposal + coverage) (#14426)

* fix: initialize Print::m_isBBLPrinter

Built outside the GUI/CLI (headless tests, embedded use) the member was read
uninitialized: is_BBL_printer()/wipe_tower_type() feed it into ToolOrdering,
which then non-deterministically dropped per-feature filament assignments.
Default it to false, the value the GUI and CLI already assign for non-Bambu
printers.

* docs(test): add the fff_print testing contract

tests/fff_print/README.md codifies how the suite is organized: one file per
subsystem (each owning both in-memory and emitted-G-code assertions), flat
behavioral test names with a single [Subsystem] tag, a robust-tests guide,
the shared helpers, and an add-a-test checklist. Linked from tests/CLAUDE.md.

* test(fff_print): reorganize the suite to the contract and add coverage

Bring every subsystem into one file per the README: rename the test_data
harness to test_helpers; consolidate skirt/brim; split multi-filament and
cooling into their own files; disperse the test_printgcode grab-bag and the
end-to-end smoke scenario into focused tests; fold test_gcode into
test_gcodewriter. Standardize names and tags, align cube tests on the cube()
helper, and de-qualify the flagship files.

New coverage: multi-filament per-feature and per-object routing; a skirt/brim
behavior matrix (the #14333 rework, including brim ears, with regression
coverage for #14319 and #14366); resolved extrusion-width and config
comments; custom-G-code placeholders; fan control and speed-marker
consumption.

Re-enable three slice tests previously tagged [NotWorking]: the clipper
"Coordinate outside allowed range" error that disabled them was specific to a
past CI runner environment and no longer reproduces.

* test(fff_print): tag arm64-flaky skirt/brim tests NotWorking

Four skirt/brim slice tests intermittently throw ClipperLib's "Coordinate
outside allowed range" on the macOS and Windows arm64 CI toolchains (an FP
divergence, not a slicing bug; see PR #14207). Linux x86_64 and aarch64 are
unaffected. Tag them [NotWorking] so ctest -LE NotWorking skips them.

* test(fff_print): re-enable the arm64 skirt/brim tests

These were tagged [NotWorking] as a stopgap when myfork's daily-driver build
combined them with the cross-platform CI on a base that predated upstream's
m_origin fix (99dea01cc3). With upstream merged in, Print::m_origin is
initialized and the "Coordinate outside allowed range" throw is gone, so the
tests pass on macOS/Windows arm64. Drop the tags.
This commit is contained in:
raistlin7447
2026-07-06 09:24:24 -05:00
committed by GitHub
parent a1ff45284c
commit 29f31b9b38
21 changed files with 1260 additions and 1034 deletions

View File

@@ -1154,7 +1154,7 @@ private:
PrintRegionPtrs m_print_regions; PrintRegionPtrs m_print_regions;
//SoftFever //SoftFever
bool m_isBBLPrinter; bool m_isBBLPrinter = false;
// Ordered collections of extrusion paths to build skirt loops and brim. // Ordered collections of extrusion paths to build skirt loops and brim.
ExtrusionEntityCollection m_skirt; ExtrusionEntityCollection m_skirt;

View File

@@ -2,6 +2,11 @@
This guide provides comprehensive instructions for Claude Code when writing, maintaining, and understanding tests in the OrcaSlicer codebase. This guide provides comprehensive instructions for Claude Code when writing, maintaining, and understanding tests in the OrcaSlicer codebase.
> **Adding or organizing `fff_print` tests?** See
> [fff_print/README.md](fff_print/README.md) for where a test belongs and how to
> name it. This guide covers Catch2 mechanics; that README is the suite's
> organizing contract.
## ⚠️ CRITICAL RULES - MUST FOLLOW ## ⚠️ CRITICAL RULES - MUST FOLLOW
### 1. **SECTIONS IN LOOPS - NEVER REUSE NAMES** ### 1. **SECTIONS IN LOOPS - NEVER REUSE NAMES**

View File

@@ -1,17 +1,17 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME) get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp ${_TEST_NAME}_tests.cpp
test_data.cpp test_helpers.cpp
test_data.hpp test_helpers.hpp
test_cooling.cpp
test_extrusion_entity.cpp test_extrusion_entity.cpp
test_fill.cpp test_fill.cpp
test_flow.cpp test_flow.cpp
test_gcode.cpp
test_gcode_timing.cpp test_gcode_timing.cpp
test_gcodewriter.cpp test_gcodewriter.cpp
test_model.cpp test_model.cpp
test_multifilament.cpp
test_print.cpp test_print.cpp
test_printgcode.cpp
test_printobject.cpp test_printobject.cpp
test_skirt_brim.cpp test_skirt_brim.cpp
test_support_material.cpp test_support_material.cpp

93
tests/fff_print/README.md Normal file
View File

@@ -0,0 +1,93 @@
# fff_print test suite
Component- and pipeline-level tests for FFF slicing: the path from a `Model` plus config, through `Print` / `PrintObject`, to emitted G-code.
For Catch2 mechanics (assertions, generators, matchers, random ordering, thread-safety), see [../CLAUDE.md](../CLAUDE.md). This document is the organizing contract for the suite: where a test goes, and how it is named.
## Organizing principle
**One file per subsystem. A subsystem is usually a single production class (`Flow`, `PrintObject`), but may be a cohesive feature that spans several (skirt/brim lives in `Brim.cpp`, `Print.cpp`, and `GCode.cpp`). That file owns every test for the subsystem: in-memory-state assertions and emitted-G-code assertions alike.**
A test's home is decided by *what production code it exercises*, never by *how it observes the result*. A skirt test that inspects `print.skirt()` and one that greps the G-code for `; skirt` live in the same file.
If you touched code in a subsystem, its test file is where your test goes. If a subsystem has no file yet, add `test_<subsystem>.cpp` and list it in `CMakeLists.txt`.
## File ownership
### Building blocks (one class, exercised through its API)
| File | Source (`src/libslic3r/`) | Covers |
|---|---|---|
| `test_trianglemesh` | `TriangleMesh.{c,h}pp` | mesh stats, transforms, slicing, split/merge/cut |
| `test_flow` | `Flow.{c,h}pp` | extrusion width / area math |
| `test_extrusion_entity` | `ExtrusionEntity.{c,h}pp` | extrusion-collection geometry |
| `test_gcodewriter` | `GCodeWriter.{c,h}pp`, `GCode.cpp` | low-level G-code emit primitives, origin |
| `test_model` | `Model.{c,h}pp` | object / volume / instance construction |
### Slicing pipeline (build a `Print`, then assert state or G-code)
| File | Source (`src/libslic3r/`) | Covers |
|---|---|---|
| `test_printobject` | `PrintObject.cpp` | layer heights, perimeter generation |
| `test_fill` | `Fill/` | infill patterns and infill G-code |
| `test_skirt_brim` | `Brim.cpp`, `Print.cpp` | skirt/brim loop counts, grouping, brim ears, emission order |
| `test_support_material` | `Support/` | support & raft layers, contact distance |
| `test_cooling` | `GCode/CoolingBuffer.cpp` | fan control, speed-marker consumption |
| `test_multifilament` | `GCode/ToolOrdering.cpp` | per-feature and per-object filament routing |
| `test_print` | `Print.{c,h}pp` | `validate()`, solid-shell behavior, sequential printing, custom G-code & config comments, default-slice smoke |
Paths are under `src/libslic3r/`. A trailing `/` is a directory of related files; otherwise it is a single class. `{c,h}pp` means the `.cpp`/`.hpp` pair.
## Naming and tags
- **File:** `test_<subsystem>.cpp`.
- **Test name:** a plain behavioral sentence, present tense, stating the contract the test pins down. No `Subsystem:` prefix (the tag carries that).
- Good: `TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")`
- Avoid: `TEST_CASE("Print: Skirt generation", "[Print]")`
- **Tags:**
- Exactly one **subsystem** tag, PascalCase, matching the file (`[SkirtBrim]`, `[PrintObject]`, `[Fill]`). This is the grouping / filter key.
- Optional **cross-cutting** tags for a concern that genuinely spans files (`[validate]`, `[Regression]`).
- **Status** tags: `[NotWorking]` marks a test disabled for a known, documented reason; CI excludes it via `~[NotWorking]` (it does not hide itself). Use `[.]` to hide a test from default runs entirely. Either way, say why in a one-line comment.
## Test style
Prefer a flat `TEST_CASE` per behavior, with `GENERATE` for parameterized cases and shared setup factored into helpers. The test name carries the behavior, so the BDD scaffolding is usually redundant. Reserve `SCENARIO` / `GIVEN` / `WHEN` / `THEN` for a test with genuine shared setup that branches into a few closely related variations, and never let a `SCENARIO` accumulate unrelated `WHEN`s: that grab-bag is what this contract exists to prevent (and it hides failures behind a single coarse test case).
## Robust tests
A test should fail only when the behavior it names breaks, not from unrelated changes (the "change-detector" anti-pattern). Test behavior, not incidentals, and aim for one reason to fail. Concretely:
- Don't depend on or assert defaults: set the config keys the behavior needs, and derive expected values from those inputs (a 20mm cube at 0.2mm = 100 layers), not from a default that may change.
- Assert the defining property, not an incidental value: prefer "skirt present", "at least 2 brim loops", or "ears vs none" over exact coordinates, extrusion amounts, line counts, or byte sizes.
- Compare floats with a tolerance (`WithinAbs` / `WithinRel`), never `==`.
- Match the meaningful G-code token (`; skirt`), not whole lines, whitespace, or comment wording.
- Rely on ordering only when it is the contract (as `role_sequence` does).
- Keep tests self-contained: no shared state, green under `--order rand`.
## Helpers
Reuse these instead of building a `Print` or parsing G-code by hand.
- **Global** (`tests/test_utils.hpp`, available to every suite):
- `load_model("file.obj")`: load a `TriangleMesh` from `tests/data/`.
- `ScopedTemporaryFile`: an RAII temp-file path, removed on scope exit.
- **Suite harness** (`fff_print/test_helpers.{hpp,cpp}`):
- Build and run: `init_print(...)`, `init_and_process_print(...)`, `slice(...)` (returns the G-code string), and `gcode(print)`.
- Two-cube placement: `slice_two_cubes_arranged(...)` (arranger-positioned), and `place_two_cubes_apart(...)` / `slice_two_cubes_apart(...)` (a fixed gap, not arranged).
- Meshes: `cube(size)` / `make_cube(...)` for simple shapes; the `TestMesh` enum with `mesh(...)` for named fixtures.
- G-code analysis: `layers_with_role(gcode, role)`, `max_z(gcode)`, `role_passes(gcode, role)`, `role_sequence(gcode, roles)`. Subsystem-specific checks stay local (for example `brim_count` in `test_skirt_brim`).
Promote a helper into the suite harness when it is a general test primitive (not tied to one subsystem's logic), even if only one file uses it today; keep genuinely subsystem-specific helpers local (file-static). Reuse potential, not current usage count, is the test.
## Adding a test (checklist)
1. Find the subsystem's file in the tables; create `test_<subsystem>.cpp` if missing.
2. Build the print with a harness helper; set only the config keys the behavior needs.
3. Assert the behavior, in-memory or via parsed G-code, whichever is clearest.
4. Name it as a behavioral sentence and tag it `[Subsystem]`.
5. For a bug fix, add the regression test in the owning file. Name it for the behavior it protects; the test must stand on its own without relying on an external issue or PR for meaning.
## Running
ctest --test-dir build/tests/fff_print
build/tests/fff_print/<config>/fff_print_tests --order rand "~[NotWorking]"

View File

@@ -0,0 +1,27 @@
#include <catch2/catch_all.hpp>
#include "test_helpers.hpp"
#include <string>
using namespace Slic3r;
using namespace Slic3r::Test;
// The fan is held off for the first close_fan_the_first_x_layers layers, so an explicit
// fan-off command is emitted.
TEST_CASE("Fan is held off for the initial layers", "[Cooling]")
{
const std::string gcode = slice({ cube(20) }, {
{ "cooling", true },
{ "close_fan_the_first_x_layers", 5 },
});
CHECK(gcode.find("M106 S0") != std::string::npos);
}
// The cooling pass resolves and strips its internal speed placeholders; none leak into
// the final G-code.
TEST_CASE("Cooling consumes its internal speed markers", "[Cooling]")
{
const std::string gcode = slice({ cube(20) }, { { "layer_height", 0.2 } });
CHECK(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
}

View File

@@ -1,95 +0,0 @@
#ifndef SLIC3R_TEST_DATA_HPP
#define SLIC3R_TEST_DATA_HPP
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <set>
#include <string>
#include <unordered_map>
namespace Slic3r { namespace Test {
constexpr double MM_PER_MIN = 60.0;
/// Enumeration of test meshes
enum class TestMesh {
A,
L,
V,
_40x10,
cube_20x20x20,
sphere_50mm,
bridge,
bridge_with_hole,
cube_with_concave_hole,
cube_with_hole,
gt2_teeth,
ipadstand,
overhang,
pyramid,
sloping_hole,
slopy_cube,
small_dorito,
step,
two_hollow_squares
};
// Necessary for <c++17
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
};
/// Mesh enumeration to name mapping
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
/// Port of Slic3r::Test::mesh
/// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it
TriangleMesh mesh(TestMesh m);
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
/// Templated function to see if two values are equivalent (+/- epsilon)
template <typename T>
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string gcode(Print& print);
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
// 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
#endif // SLIC3R_TEST_DATA_HPP

View File

@@ -7,7 +7,7 @@
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "test_data.hpp" #include "test_helpers.hpp"
using namespace Slic3r; using namespace Slic3r;
@@ -35,7 +35,7 @@ static Slic3r::ExtrusionPaths random_paths(size_t count = 10, size_t length = 20
return p; return p;
} }
SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]") { SCENARIO("Polygon flattening", "[ExtrusionEntity]") {
srand(0xDEADBEEF); // consistent seed for test reproducibility. srand(0xDEADBEEF); // consistent seed for test reproducibility.
// Generate one specific random path set and save it for later comparison // Generate one specific random path set and save it for later comparison

View File

@@ -11,14 +11,14 @@
#include "libslic3r/SVG.hpp" #include "libslic3r/SVG.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "test_data.hpp" #include "test_helpers.hpp"
using namespace Slic3r; using namespace Slic3r;
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0); bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
#if 0 #if 0
TEST_CASE("Fill: adjusted solid distance") { TEST_CASE("Adjusted solid distance", "[Fill]") {
int surface_width = 250; int surface_width = 250;
int distance = Slic3r::Flow::solid_spacing(surface_width, 47); int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
REQUIRE(distance == Catch::Approx(50)); REQUIRE(distance == Catch::Approx(50));
@@ -26,7 +26,7 @@ TEST_CASE("Fill: adjusted solid distance") {
} }
#endif #endif
TEST_CASE("Fill: Pattern Path Length", "[Fill]") { TEST_CASE("Pattern path length", "[Fill]") {
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear")); std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
filler->angle = float(-(PI)/2.0); filler->angle = float(-(PI)/2.0);
FillParams fill_params; FillParams fill_params;

View File

@@ -3,7 +3,7 @@
#include <numeric> #include <numeric>
#include <sstream> #include <sstream>
#include "test_data.hpp" // get access to init_print, etc #include "test_helpers.hpp" // get access to init_print, etc
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
@@ -17,7 +17,7 @@ using namespace Slic3r;
/// Test the expected behavior for auto-width, /// Test the expected behavior for auto-width,
/// spacing, etc /// spacing, etc
SCENARIO("Flow: Flow math for non-bridges", "[Flow]") { SCENARIO("Flow math for non-bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") { GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
ConfigOptionFloatOrPercent width(1.0, false); ConfigOptionFloatOrPercent width(1.0, false);
float nozzle_diameter = 0.4f; float nozzle_diameter = 0.4f;
@@ -79,7 +79,7 @@ SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
} }
/// Spacing, width calculation for bridge extrusions /// Spacing, width calculation for bridge extrusions
SCENARIO("Flow: Flow math for bridges", "[Flow]") { SCENARIO("Flow math for bridges", "[Flow]") {
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") { GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
float nozzle_diameter = 0.4f; float nozzle_diameter = 0.4f;
float bridge_flow = 1.0f; float bridge_flow = 1.0f;

View File

@@ -1,22 +0,0 @@
#include <catch2/catch_all.hpp>
#include <memory>
#include "libslic3r/GCode.hpp"
using namespace Slic3r;
SCENARIO("Origin manipulation", "[GCode]") {
Slic3r::GCode gcodegen;
WHEN("set_origin to (10,0)") {
gcodegen.set_origin(Vec2d(10,0));
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
}
WHEN("set_origin to (10,0) and translate by (5, 5)") {
gcodegen.set_origin(Vec2d(10,0));
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
THEN("origin returns reference to point") {
REQUIRE(gcodegen.origin() == Vec2d(15,5));
}
}
}

View File

@@ -3,8 +3,15 @@
#include <memory> #include <memory>
#include "libslic3r/GCodeWriter.hpp" #include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "test_helpers.hpp"
using namespace Slic3r; using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") { SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
@@ -57,3 +64,343 @@ SCENARIO("z_hop lifts the nozzle when a lift is requested", "[GCodeWriter]") {
} }
} }
} }
SCENARIO("Origin manipulation", "[GCodeWriter]") {
Slic3r::GCode gcodegen;
WHEN("set_origin to (10,0)") {
gcodegen.set_origin(Vec2d(10,0));
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
}
WHEN("set_origin to (10,0) and translate by (5, 5)") {
gcodegen.set_origin(Vec2d(10,0));
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
THEN("origin returns reference to point") {
REQUIRE(gcodegen.origin() == Vec2d(15,5));
}
}
}
// Verify that emit_machine_limits_to_gcode emits the correct max value across
// used extruders (regression for commit b4ee665: "Emit max value of machine
// limit among used extruders").
TEST_CASE("Machine envelope emits max limit among used extruders", "[GCodeWriter]")
{
SECTION("Single extruder emits its configured values") {
const std::string gcode = Slic3r::Test::slice({ cube(20) }, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin2" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// stride-2 options: (normal, silent)
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "700,800" },
{ "machine_max_acceleration_z", "100,200" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
// stride-2 options: (normal, silent)
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("M201 uses the normal acceleration values") {
REQUIRE(gcode.find("M201 X500 Y700 Z100 E5000") != std::string::npos);
}
THEN("M203 uses the speed values") {
REQUIRE(gcode.find("M203 X100 Y110 Z10 E50") != std::string::npos);
}
THEN("M204 (Marlin 2) uses extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P1200 R1400 T1600") != std::string::npos);
}
THEN("M205 uses the jerk values") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
THEN("M205 J uses the junction deviation") {
REQUIRE(gcode.find("M205 J0.020") != std::string::npos);
}
}
SECTION("Legacy Marlin flavor emits correct format") {
const std::string gcode = Slic3r::Test::slice({ cube(20) }, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// All machine limits must be provided — defaults are empty vectors.
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "500,600" },
{ "machine_max_acceleration_z", "500,600" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("Legacy Marlin: M204 travel_acc = extruding_acc") {
// gcfMarlinLegacy uses extruding acc for travel too
REQUIRE(gcode.find("M204 P1200 R1400 T1200") != std::string::npos);
}
THEN("Legacy Marlin: M205 uses mm/sec format") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
}
SECTION("Multi extruder - max of used extruders is emitted") {
// Build config with 2 extruders that have *different* machine limits.
// Extruder 1 has higher values; the emitted G-code must use the max.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
// Print basics
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
// Print objects sequentially so each uses its own extruder without
// wipe-tower / tool-change complexity.
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
// filament_map maps filament slot index (1-based) → logical extruder ID (1-based).
// Default [1] maps everything to extruder 0. Need [1, 2] for two distinct extruders.
// fmmManual prevents auto-computation from overwriting the explicit mapping.
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
// flush_volumes_matrix must be filament_count^2 * heads_count entries.
// 2 filaments * 2 * 1 head = 4 entries (all zero — flush volumes not tested here).
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// Machine limits: extruder 0 low, extruder 1 high
// Stride-2 (normal, silent pairs): e0_n, e0_s, e1_n, e1_s
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({700, 0, 1100, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 300, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 8000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({1200, 0, 2200, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({1400, 0, 2400, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({1600, 0, 2600, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({8, 0, 12, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({9, 0, 13, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.6, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({5, 0, 10, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.05, 0}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
// obj1 uses default extruder=1 (0-based index 0)
auto* obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{},
ArrangeParams{scaled(min_object_distance(config))});
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
std::string gcode = Slic3r::Test::gcode(print);
THEN("M201 contains max (extruder 1's) acceleration values") {
REQUIRE(gcode.find("M201 X1000 Y1100 Z300 E8000") != std::string::npos);
}
THEN("M203 contains max speed values") {
REQUIRE(gcode.find("M203 X200 Y210 Z30 E80") != std::string::npos);
}
THEN("M204 contains max extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P2200 R2400 T2600") != std::string::npos);
}
THEN("M205 contains max jerk values") {
REQUIRE(gcode.find("M205 X12.00 Y13.00 Z0.60 E10.00") != std::string::npos);
}
THEN("M205 contains max m_max_junction_deviation ") {
REQUIRE(gcode.find("M205 J0.050") != std::string::npos);
}
}
}
// Verify that the EXTRUDER_LIMIT macro (GCodeWriter.cpp) correctly:
// 1) Uses the active extruder's specific limit when filament() is known.
// 2) Falls back to the maximum of all extruder limits when filament() is nullptr.
//
// These two behaviours were introduced in:
// - "Use per-extruder motion limit" (1ab34a7454)
// - "Use max limit when current extruder is unknown" (b7240ab1c6)
TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[GCodeWriter]")
{
// --- Build config with 2 extruders that have different machine limits ---
// Extruder 0: low limits
// Extruder 1: high limits
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders, 2 filaments
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// --- Machine limits (stride-2: e0_n, e0_s, e1_n, e1_s) ---
// Extruder 0 has LOW limits, Extruder 1 has HIGH limits.
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 5000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({500, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({600, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({700, 0, 2500, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({5, 0, 15, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({6, 0, 16, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.8, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({3, 0, 8, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.08, 0}));
// --- Print acceleration: 1500 mm/s² ---
// Exceeds extruder 0's limit (500) → should be clamped to 500.
// Does NOT exceed extruder 1's limit (2000) → passes through as 1500.
config.set_key_value("default_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("outer_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("inner_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("top_surface_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("initial_layer_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("travel_acceleration", new ConfigOptionFloats({1500, 1500}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
auto* obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))});
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
std::string gcode = Slic3r::Test::gcode(print);
SECTION("Preamble: max limit among used extruders") {
THEN("M201 uses max (extruder 1's) acceleration values") {
REQUIRE(gcode.find("M201 X1000 Y1000 Z200 E5000") != std::string::npos);
}
THEN("M204 uses max extruding/retracting/travel") {
REQUIRE(gcode.find("M204 P2000 R2000 T2500") != std::string::npos);
}
THEN("M205 uses max jerk values") {
REQUIRE(gcode.find("M205 X15.00 Y16.00 Z0.80 E8.00") != std::string::npos);
}
}
SECTION("Preamble: EXTRUDER_LIMIT falls back to max when no filament is active") {
// set_junction_deviation() is called during preamble with no active filament.
// EXTRUDER_LIMIT(m_max_junction_deviation) → filament() == nullptr → max of all (0.08).
THEN("M205 J uses max junction deviation") {
REQUIRE(gcode.find("M205 J0.080") != std::string::npos);
}
}
SECTION("Print: extruder 0 acceleration clamped to its specific limit") {
// Extruder 0 machine limit = 500. Print accel = 1500 > 500 → clamped to 500.
THEN("M204 P500 appears (extruder 0 clamped)") {
REQUIRE(gcode.find("M204 P500") != std::string::npos);
}
THEN("M204 T700 appears (extruder 0 travel clamped)") {
REQUIRE(gcode.find("M204 T700") != std::string::npos);
}
}
SECTION("Print: extruder 1 acceleration NOT clamped to extruder 0's limit") {
// Extruder 1 machine limit = 2000. Print accel = 1500 < 2000 → not clamped.
THEN("M204 P1500 appears (extruder 1 not clamped to 500)") {
REQUIRE(gcode.find("M204 P1500") != std::string::npos);
}
}
}

View File

@@ -1,4 +1,4 @@
#include "test_data.hpp" #include "test_helpers.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCodeReader.hpp"
@@ -19,13 +19,11 @@ using namespace std;
namespace Slic3r { namespace Test { namespace Slic3r { namespace Test {
// Mesh enumeration to name mapping
const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names { const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names {
std::pair<TestMesh, const char*>(TestMesh::A, "A"), std::pair<TestMesh, const char*>(TestMesh::A, "A"),
std::pair<TestMesh, const char*>(TestMesh::L, "L"), std::pair<TestMesh, const char*>(TestMesh::L, "L"),
std::pair<TestMesh, const char*>(TestMesh::V, "V"), std::pair<TestMesh, const char*>(TestMesh::V, "V"),
std::pair<TestMesh, const char*>(TestMesh::_40x10, "40x10"), std::pair<TestMesh, const char*>(TestMesh::_40x10, "40x10"),
std::pair<TestMesh, const char*>(TestMesh::cube_20x20x20, "cube_20x20x20"),
std::pair<TestMesh, const char*>(TestMesh::sphere_50mm, "sphere_50mm"), std::pair<TestMesh, const char*>(TestMesh::sphere_50mm, "sphere_50mm"),
std::pair<TestMesh, const char*>(TestMesh::bridge, "bridge"), std::pair<TestMesh, const char*>(TestMesh::bridge, "bridge"),
std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole, "bridge_with_hole"), std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole, "bridge_with_hole"),
@@ -46,9 +44,6 @@ TriangleMesh mesh(TestMesh m)
{ {
TriangleMesh mesh; TriangleMesh mesh;
switch(m) { switch(m) {
case TestMesh::cube_20x20x20:
mesh = Slic3r::make_cube(20, 20, 20);
break;
case TestMesh::sphere_50mm: case TestMesh::sphere_50mm:
mesh = Slic3r::make_sphere(50, PI / 243.0); mesh = Slic3r::make_sphere(50, PI / 243.0);
break; break;
@@ -187,30 +182,72 @@ TriangleMesh mesh(TestMesh m)
return mesh; return mesh;
} }
static bool verbose_gcode() Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh)
{ {
const char *v = std::getenv("SLIC3R_TESTS_GCODE"); Slic3r::Model result;
if (v == nullptr) ModelObject *object = result.add_object();
return false; object->name += model_name + ".stl";
std::string s(v); object->add_volume(_mesh);
return s == "1" || s == "on" || s == "yes"; object->add_instance();
return result;
} }
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments) DynamicPrintConfig multifilament_config(unsigned int filaments, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra)
{
// Single nozzle, `filaments` filaments. Colours must be DISTINCT: filament grouping
// treats same-colour filaments as one and the tool-order path then drops/segfaults.
static const char *palette[] = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00",
"#FF00FF", "#00FFFF", "#FF8000", "#8000FF" };
std::string diameters, colours, flush;
for (unsigned int i = 0; i < filaments; ++i) {
diameters += (i ? "," : "") + std::string("1.75");
colours += (i ? ";" : "") + std::string(palette[i % (sizeof(palette) / sizeof(palette[0]))]);
for (unsigned int j = 0; j < filaments; ++j)
flush += ((i || j) ? "," : "") + std::string(i == j ? "0" : "280");
}
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "nozzle_diameter", "0.4" }, { "filament_diameter", diameters } });
config.set_num_filaments(filaments);
// These are read by filament id during export but absent from filament_option_keys(),
// so set_num_filaments leaves them size 1; size them too, else out-of-range access.
const auto &defaults = FullPrintConfig::defaults();
for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode" })
static_cast<ConfigOptionVectorBase *>(config.option(key, true))->resize(filaments, defaults.option(key));
// flush_volumes_matrix must be sized filaments*filaments or export rejects it.
config.set_deserialize_strict({ { "filament_colour", colours }, { "flush_volumes_matrix", flush } });
if (extra.size() > 0)
config.set_deserialize_strict(extra);
return config;
}
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in,
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> *per_object_overrides, bool arrange)
{ {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.apply(config_in); config.apply(config_in);
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
if (verbose_gcode()) size_t object_idx = 0;
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
for (const TriangleMesh &t : meshes) { for (const TriangleMesh &t : meshes) {
ModelObject *object = model.add_object(); ModelObject *object = model.add_object();
object->name += "object.stl"; object->name += "object.stl";
object->add_volume(std::move(t)); object->add_volume(std::move(t));
object->add_instance(); object->add_instance();
if (per_object_overrides && object_idx < per_object_overrides->size() && !(*per_object_overrides)[object_idx].empty()) {
DynamicPrintConfig oc;
for (const auto &item : (*per_object_overrides)[object_idx])
oc.set_deserialize_strict(item.opt_key, item.opt_value);
object->config.apply(oc);
}
++object_idx;
} }
arrange_objects(model, InfiniteBed{}, ArrangeParams{ scaled(min_object_distance(config))}); if (arrange)
arrange_objects(model, InfiniteBed{}, ArrangeParams{ scaled(min_object_distance(config))});
for (ModelObject *mo : model.objects) { for (ModelObject *mo : model.objects) {
mo->ensure_on_bed(); mo->ensure_on_bed();
print.auto_assign_extruders(mo); print.auto_assign_extruders(mo);
@@ -221,63 +258,63 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
print.set_status_silent(); print.set_status_silent();
} }
void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in, bool comments) void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in)
{ {
std::vector<TriangleMesh> triangle_meshes; std::vector<TriangleMesh> triangle_meshes;
triangle_meshes.reserve(test_meshes.size()); triangle_meshes.reserve(test_meshes.size());
for (const TestMesh test_mesh : test_meshes) for (const TestMesh test_mesh : test_meshes)
triangle_meshes.emplace_back(mesh(test_mesh)); triangle_meshes.emplace_back(mesh(test_mesh));
init_print(std::move(triangle_meshes), print, model, config_in, comments); init_print(std::move(triangle_meshes), print, model, config_in);
} }
void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments) void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in)
{ {
std::vector<TriangleMesh> triangle_meshes; std::vector<TriangleMesh> triangle_meshes;
triangle_meshes.reserve(input_meshes.size()); triangle_meshes.reserve(input_meshes.size());
for (const TriangleMesh &input_mesh : input_meshes) for (const TriangleMesh &input_mesh : input_meshes)
triangle_meshes.emplace_back(input_mesh); triangle_meshes.emplace_back(input_mesh);
init_print(std::move(triangle_meshes), print, model, config_in, comments); init_print(std::move(triangle_meshes), print, model, config_in);
} }
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments) void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{ {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict(config_items); config.set_deserialize_strict(config_items);
init_print(meshes, print, model, config, comments); init_print(meshes, print, model, config);
} }
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments) void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{ {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict(config_items); config.set_deserialize_strict(config_items);
init_print(meshes, print, model, config, comments); init_print(meshes, print, model, config);
} }
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments) void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config)
{ {
Slic3r::Model model; Slic3r::Model model;
init_print(meshes, print, model, config, comments); init_print(meshes, print, model, config);
print.process(); print.process();
} }
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments) void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config)
{ {
Slic3r::Model model; Slic3r::Model model;
init_print(meshes, print, model, config, comments); init_print(meshes, print, model, config);
print.process(); print.process();
} }
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments) void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{ {
Slic3r::Model model; Slic3r::Model model;
init_print(meshes, print, model, config_items, comments); init_print(meshes, print, model, config_items);
print.process(); print.process();
} }
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments) void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{ {
Slic3r::Model model; Slic3r::Model model;
init_print(meshes, print, model, config_items, comments); init_print(meshes, print, model, config_items);
print.process(); print.process();
} }
@@ -292,6 +329,76 @@ std::string gcode(Print & print)
return str; return str;
} }
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config);
return gcode(print);
}
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config);
return gcode(print);
}
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config_items);
return gcode(print);
}
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config_items);
return gcode(print);
}
std::string slice_with_object_overrides(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config,
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> &per_object_overrides)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(std::vector<TriangleMesh>(meshes), print, model, config, &per_object_overrides);
return gcode(print);
}
std::string slice_two_cubes_arranged(std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{
return slice({ cube(20), cube(20) }, config_items);
}
void place_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
Print &print, Model &model)
{
TriangleMesh a = cube(20);
a.translate(80, 80, 0);
TriangleMesh b = cube(20);
b.translate(80 + 20 + gap, 80, 0);
std::vector<TriangleMesh> meshes;
meshes.push_back(std::move(a));
meshes.push_back(std::move(b));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict(config_items);
init_print(std::move(meshes), print, model, config, nullptr, /*arrange=*/false);
}
std::string slice_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
{
Print print;
Model model;
place_two_cubes_apart(gap, config_items, print, model);
return gcode(print);
}
std::set<double> layers_with_role(const std::string &gcode, const std::string &role) std::set<double> layers_with_role(const std::string &gcode, const std::string &role)
{ {
std::set<double> layers; std::set<double> layers;
@@ -313,59 +420,48 @@ double max_z(const std::string &gcode)
return z; return z;
} }
Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh) int role_passes(const std::string &gcode, const std::string &role)
{ {
Slic3r::Model result; int passes = 0;
ModelObject *object = result.add_object(); bool in_role = false;
object->name += model_name + ".stl"; GCodeReader reader;
object->add_volume(_mesh); reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
object->add_instance(); if (! line.extruding(self)) return;
return result; const bool is_role = line.comment().find(role) != std::string_view::npos;
if (is_role && ! in_role) ++passes;
in_role = is_role;
});
return passes;
} }
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments) std::vector<std::string> role_sequence(const std::string &gcode, const std::vector<std::string> &roles)
{ {
Slic3r::Print print; std::vector<std::string> seq;
Slic3r::Model model; std::string current;
init_print(meshes, print, model, config, comments); GCodeReader reader;
return gcode(print); reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
} if (! line.extruding(self)) return;
const std::string_view comment = line.comment();
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments) for (const std::string &role : roles)
{ if (comment.find(role) != std::string_view::npos) {
Slic3r::Print print; if (current != role) { seq.push_back(role); current = role; }
Slic3r::Model model; break;
init_print(meshes, print, model, config, comments); }
return gcode(print); });
} return seq;
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config_items, comments);
return gcode(print);
}
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
{
Slic3r::Print print;
Slic3r::Model model;
init_print(meshes, print, model, config_items, comments);
return gcode(print);
} }
} } // namespace Slic3r::Test } } // namespace Slic3r::Test
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
SCENARIO("init_print functionality", "[test_data]") { SCENARIO("init_print functionality", "[test_helpers]") {
GIVEN("A default config") { GIVEN("A default config") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
WHEN("init_print is called with a single mesh.") { WHEN("init_print is called with a single mesh.") {
Slic3r::Model model; Slic3r::Model model;
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_print({ Slic3r::Test::TestMesh::cube_20x20x20 }, print, model, config, true); Slic3r::Test::init_print({ Slic3r::Test::cube(20) }, print, model, config);
THEN("One mesh/printobject is in the resulting Print object.") { THEN("One mesh/printobject is in the resulting Print object.") {
REQUIRE(print.objects().size() == 1); REQUIRE(print.objects().size() == 1);
} }

View File

@@ -0,0 +1,125 @@
#ifndef SLIC3R_TEST_HELPERS_HPP
#define SLIC3R_TEST_HELPERS_HPP
#include "libslic3r/Config.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
namespace Slic3r { namespace Test {
constexpr double MM_PER_MIN = 60.0;
// True when `a` and `b` are within EPSILON.
template <typename T>
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
// True when `a` and `b` are within `epsilon`.
template <typename T>
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
// Named reusable test meshes, resolved by mesh().
enum class TestMesh {
A,
L,
V,
_40x10,
sphere_50mm,
bridge,
bridge_with_hole,
cube_with_concave_hole,
cube_with_hole,
gt2_teeth,
ipadstand,
overhang,
pyramid,
sloping_hole,
slopy_cube,
small_dorito,
step,
two_hollow_squares
};
// Hash for TestMesh (std::hash lacks scoped-enum support before C++17).
struct TestMeshHash {
std::size_t operator()(TestMesh tm) const {
return static_cast<std::size_t>(tm);
}
};
// TestMesh value to name mapping.
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
// Geometry for the named test fixture `m`, optionally translated and scaled.
TriangleMesh mesh(TestMesh m);
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
// An equal-sided cube, `size` mm on each edge.
inline TriangleMesh cube(double size) { return make_cube(size, size, size); }
// A Model holding one object built from `mesh`.
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
// Single-nozzle, `filaments`-filament config from defaults; `extra` is applied last.
DynamicPrintConfig multifilament_config(unsigned int filaments,
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {});
// Apply `meshes` and config to `print`/`model`; optional per-object overrides, auto-arranged unless `arrange` is false.
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in,
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> *per_object_overrides = nullptr, bool arrange = true);
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config());
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config());
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// init_print followed by process(), leaving a sliced `print` to inspect.
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config);
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// Process `print` and return its exported G-code.
std::string gcode(Print& print);
// Build, slice, and return the G-code for `meshes` under the given config.
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config);
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config);
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// Slice `meshes`, applying per_object_overrides[i] to object i first (empty entry = none).
std::string slice_with_object_overrides(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config,
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> &per_object_overrides);
// Slice two auto-arranged 20mm cubes (the arranger positions them).
std::string slice_two_cubes_arranged(std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// Place two 20mm cubes `gap` mm apart edge-to-edge, not auto-arranged (the caller controls spacing).
void place_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
Slic3r::Print &print, Slic3r::Model &model);
// Slice two 20mm cubes `gap` mm apart (not auto-arranged) and return the G-code.
std::string slice_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
// Distinct layer Z heights carrying an extrusion of the given `role` (e.g. "skirt").
std::set<double> layers_with_role(const std::string &gcode, const std::string &role);
// Highest Z reached by any move in the G-code.
double max_z(const std::string &gcode);
// Count of contiguous extrusion blocks of `role` (each uninterrupted run counts once).
int role_passes(const std::string &gcode, const std::string &role);
// The `roles` in the order their extrusion blocks first appear, consecutive repeats collapsed.
std::vector<std::string> role_sequence(const std::string &gcode, const std::vector<std::string> &roles);
} } // namespace Slic3r::Test
#endif // SLIC3R_TEST_HELPERS_HPP

View File

@@ -6,7 +6,7 @@
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include "test_data.hpp" #include "test_helpers.hpp"
#include "test_utils.hpp" #include "test_utils.hpp"
using namespace Slic3r; using namespace Slic3r;

View File

@@ -0,0 +1,87 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "test_helpers.hpp"
#include <cctype>
#include <set>
#include <string>
using namespace Slic3r;
using namespace Slic3r::Test;
// 0-based tool indices used by extrusions whose role comment contains `role` (needs gcode_comments).
static std::set<int> tools_for_role(const std::string& gcode, const std::string& role)
{
std::set<int> tools;
int current_tool = 0;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader& self, const GCodeReader::GCodeLine& line) {
const std::string cmd(line.cmd());
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
current_tool = std::stoi(cmd.substr(1));
else if (line.extruding(self) && std::string(line.comment()).find(role) != std::string::npos)
tools.insert(current_tool);
});
return tools;
}
// Tool index = filament id - 1; brim and skirt follow the wall filament.
TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]")
{
auto [infill_filament, wall_filament] = GENERATE(table<int, int>({ {1, 1}, {1, 2}, {2, 1}, {2, 2} }));
DYNAMIC_SECTION("infill filament " << infill_filament << ", wall filament " << wall_filament) {
const std::string gcode = slice({ cube(20) },
multifilament_config(2, {
{ "sparse_infill_filament_id", infill_filament },
{ "internal_solid_filament_id", infill_filament },
{ "top_surface_filament_id", infill_filament },
{ "bottom_surface_filament_id", infill_filament },
{ "outer_wall_filament_id", wall_filament },
{ "inner_wall_filament_id", wall_filament },
{ "skirt_loops", 1 },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
}));
const std::set<int> wall_tool{ wall_filament - 1 };
const std::set<int> infill_tool{ infill_filament - 1 };
CHECK(tools_for_role(gcode, "perimeter") == wall_tool);
CHECK(tools_for_role(gcode, "infill") == infill_tool); // sparse + solid + top/bottom
CHECK(tools_for_role(gcode, "brim") == wall_tool);
CHECK(tools_for_role(gcode, "skirt") == wall_tool);
}
}
TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[MultiFilament]")
{
const std::string gcode = slice({ cube(20) },
multifilament_config(3, {
{ "sparse_infill_filament_id", 2 },
{ "internal_solid_filament_id", 2 },
{ "top_surface_filament_id", 2 },
{ "bottom_surface_filament_id", 2 },
{ "outer_wall_filament_id", 3 },
{ "inner_wall_filament_id", 3 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
}));
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
}
// The override must survive tool ordering: object 1's walls print on their filament's
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
{
const std::string gcode = slice_with_object_overrides(
{ cube(20), cube(20) },
multifilament_config(2, {
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
{ "print_sequence", "by object" },
}),
{ {}, { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 } } });
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 0, 1 });
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // infill not overridden: stays on F1
}

View File

@@ -1,35 +1,32 @@
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp" #include "test_helpers.hpp"
#include "test_utils.hpp"
#include <algorithm> #include <algorithm>
#include <fstream>
#include <iterator>
using namespace Slic3r; using namespace Slic3r;
using namespace Slic3r::Test; using namespace Slic3r::Test;
SCENARIO("Print: Skirt generation", "[Print]") { SCENARIO("Changing the number of solid shell layers does not make all surfaces internal", "[Print]") {
GIVEN("20mm cube and default config") {
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 },
{ "skirt_loops", 2 }
});
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
SCENARIO("Print: Changing number of solid 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") { GIVEN("sliced 20mm cube and config with top_shell_layers = 2 and bottom_shell_layers = 1") {
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ config.set_deserialize_strict({
@@ -40,7 +37,7 @@ SCENARIO("Print: Changing number of solid shell layers does not cause all surfac
}); });
Slic3r::Print print; Slic3r::Print print;
Slic3r::Model model; Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, config); Slic3r::Test::init_print({cube(20)}, print, model, config);
// Precondition: Ensure that the model has 2 solid top layers (79, 78) // Precondition: Ensure that the model has 2 solid top layers (79, 78)
// and one solid bottom layer (0). // and one solid bottom layer (0).
auto test_is_solid_infill = [&print](size_t obj_id, size_t layer_id) { auto test_is_solid_infill = [&print](size_t obj_id, size_t layer_id) {
@@ -72,41 +69,6 @@ SCENARIO("Print: Changing number of solid shell layers does not cause all surfac
} }
} }
SCENARIO("Print: Brim generation", "[Print]") {
GIVEN("20mm cube and default config, 1mm first layer width") {
WHEN("Brim is set to 6mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "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;
for (const auto& pair : print.get_brimMap()) {
total_items += pair.second.items_count();
}
REQUIRE(total_items == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
{ "brim_type", "outer_only" },
{ "brim_width", 6 },
{ "initial_layer_line_width", 0.5 }
});
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 == 12);
}
}
}
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Print::validate() warning collection // Print::validate() warning collection
// //
@@ -129,7 +91,7 @@ void build_cubes(Slic3r::Model& model, Slic3r::Print& print,
for (int i = 0; i < n; ++i) { for (int i = 0; i < n; ++i) {
ModelObject* object = model.add_object(); ModelObject* object = model.add_object();
object->add_volume(Slic3r::Test::mesh(TestMesh::cube_20x20x20)); object->add_volume(cube(20));
ModelInstance* inst = object->add_instance(); ModelInstance* inst = object->add_instance();
inst->set_offset(Vec3d(overlap ? 0.0 : i * 60.0, 0.0, 0.0)); inst->set_offset(Vec3d(overlap ? 0.0 : i * 60.0, 0.0, 0.0));
} }
@@ -337,3 +299,122 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate
StringObjectException err = print.validate(); // warnings == nullptr StringObjectException err = print.validate(); // warnings == nullptr
CHECK(err.string.empty()); CHECK(err.string.empty());
} }
TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]")
{
const std::string gcode = slice({ cube(20) }, {
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "z_hop", 0 } // keep recorded Z at the printed height
});
CHECK(role_passes(gcode, "perimeter") > 0);
CHECK(role_passes(gcode, "infill") > 0);
CHECK(role_passes(gcode, "skirt") > 0);
CHECK_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20.0, 1e-4));
}
// The G-code carries a config-comment block describing the resolved settings. The
// per-region width lines are always present; the support and first-layer lines appear
// only when those features are configured.
TEST_CASE("G-code lists the resolved extrusion-width settings", "[Print]")
{
const std::string gcode = slice({ cube(20) }, { { "initial_layer_line_width", 0 } });
CHECK(gcode.find("; external perimeters extrusion width") != std::string::npos);
CHECK(gcode.find("; perimeters extrusion width") != std::string::npos);
CHECK(gcode.find("; infill extrusion width") != std::string::npos);
CHECK(gcode.find("; solid infill extrusion width") != std::string::npos);
CHECK(gcode.find("; top infill extrusion width") != std::string::npos);
CHECK(gcode.find("; support material extrusion width") == std::string::npos);
CHECK(gcode.find("; first layer extrusion width") == std::string::npos);
CHECK(gcode.find("; layer_height") != std::string::npos);
CHECK(gcode.find("; sparse_infill_density") != std::string::npos);
const std::string with_support = slice({ cube(20) }, {
{ "initial_layer_line_width", 0 }, { "enable_support", true }, { "raft_layers", 3 },
});
CHECK(with_support.find("; support material extrusion width") != std::string::npos);
const std::string with_first_layer = slice({ cube(20) }, { { "initial_layer_line_width", "0.5" } });
CHECK(with_first_layer.find("; first layer extrusion width") != std::string::npos);
}
// Custom G-code templates substitute placeholders during export.
TEST_CASE("Custom G-code placeholders are substituted", "[Print]")
{
// [current_extruder] in the start G-code.
CHECK(slice({ cube(20) }, { { "machine_start_gcode", "; Extruder [current_extruder]" } })
.find("; Extruder 0") != std::string::npos);
// [layer_num] / [layer_z] in the end G-code (a 20mm cube at 0.1mm is 200 layers).
const std::string end_gcode = slice({ cube(20) }, {
{ "machine_end_gcode", "; Layer_num [layer_num]\n; Layer_z [layer_z]" },
{ "layer_height", 0.1 },
{ "initial_layer_print_height", 0.1 },
});
CHECK(end_gcode.find("; Layer_num 199") != std::string::npos);
CHECK(end_gcode.find("; Layer_z 20") != std::string::npos);
// printing_by_object_gcode is emitted between sequentially printed objects.
CHECK(slice_two_cubes_arranged({
{ "print_sequence", "by object" },
{ "printing_by_object_gcode", "; between-object-gcode" },
})
.find("; between-object-gcode") != std::string::npos);
// [layer_num] keeps counting across sequentially printed objects (199 then 399).
const std::string per_layer = slice_two_cubes_arranged({
{ "print_sequence", "by object" },
{ "layer_change_gcode", ";Layer:[layer_num] ([layer_z] mm)" },
{ "layer_height", 0.1 },
{ "initial_layer_print_height", 0.1 },
});
CHECK(per_layer.find(";Layer:199 ") != std::string::npos);
CHECK(per_layer.find(";Layer:399 ") != std::string::npos);
}
TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]")
{
Print print;
Model model;
Slic3r::Test::init_print({cube(20)}, 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("Sequential printing follows model order", "[Print]")
{
// 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({ cube(20), Slic3r::make_cube(20, 20, 10) }, {
{ "print_sequence", "by object" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "z_hop", 0 }
});
// The first object's height is the peak Z reached before Z drops back to the
// first layer (the object change). With by-object printing only an object
// change returns Z to the bottom.
double first_object_peak_z = 0.0;
double running_peak = 0.0;
GCodeReader reader;
reader.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (first_object_peak_z != 0.0 || !line.extruding(self)) return; // ignore travels (e.g. start-gcode Z lift)
if (running_peak > 1.0 && self.z() < 1.0)
first_object_peak_z = running_peak;
else
running_peak = std::max(running_peak, static_cast<double>(self.z()));
});
REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3));
}

View File

@@ -1,629 +0,0 @@
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <boost/regex.hpp>
#include <libslic3r/ModelArrange.hpp>
#include <boost/filesystem.hpp>
#include <fstream>
#include <iterator>
#include <set>
using namespace Slic3r;
using namespace Slic3r::Test;
boost::regex perimeters_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; perimeter");
boost::regex infill_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; infill");
boost::regex skirt_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; skirt");
SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") {
GIVEN("A default configuration and a print test object") {
WHEN("the output is executed with no support material") {
Slic3r::Print print;
Slic3r::Model model;
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, {
{ "layer_height", 0.2 },
{ "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 git commit id") {
// REQUIRE(gcode.find("; Git Commit") != std::string::npos);
// REQUIRE(gcode.find(SLIC3R_BUILD_ID) != std::string::npos);
//}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Exported text does not contain cooling markers (they were consumed)") {
REQUIRE(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
}
THEN("The config trailer includes print and region settings") {
REQUIRE(gcode.find("; layer_height") != std::string::npos);
REQUIRE(gcode.find("; sparse_infill_density") != std::string::npos);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("final Z height is 20mm") {
REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20., 1e-4));
}
}
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, {
{ "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.") {
REQUIRE(gcode.size() > 0);
}
THEN("Infill is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
}
THEN("Perimeters are emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
}
THEN("Skirt is emitted.") {
boost::smatch has_match;
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
}
THEN("Between-object-gcode is emitted.") {
REQUIRE(gcode.find("; between-object-gcode") != std::string::npos);
}
THEN("final Z height is 20.1mm") {
REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20.1, 1e-4));
}
THEN("Z height resets on object change") {
double final_z = 0.0;
bool reset = false;
GCodeReader reader;
reader.apply_config(print.config());
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) { // saw higher Z before this, now it's lower
reset = true;
} else {
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
}
});
REQUIRE(reset == true);
}
}
WHEN("the output is executed with support material") {
std::string gcode = ::Test::slice({TestMesh::cube_20x20x20}, {
{ "initial_layer_line_width", 0 },
{ "enable_support", true },
{ "raft_layers", 3 },
{ "gcode_comments", true }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") != std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
}
THEN("Raft is emitted.") {
REQUIRE(gcode.find("; raft") != std::string::npos);
}
}
WHEN("the output is executed with a separate first layer extrusion width") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "initial_layer_line_width", "0.5" }
});
THEN("Some text output is generated.") {
REQUIRE(gcode.size() > 0);
}
THEN("Exported text contains extrusion statistics.") {
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
REQUIRE(gcode.find("; first layer extrusion width") != std::string::npos);
}
}
WHEN("Cooling is enabled and the fan is disabled.") {
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
{ "cooling", true },
{ "close_fan_the_first_x_layers", 5 }
});
THEN("GCode to disable fan is emitted."){
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 }, {
{ "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);
REQUIRE(gcode.find("; Layer_z 20") != std::string::npos);
}
}
WHEN("current_extruder exists in start_gcode") {
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 }, {
{ "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 ";
size_t pos = gcode.find(token);
THEN("First and second object last layer is emitted") {
// First object
REQUIRE(pos != std::string::npos);
pos += token.size();
REQUIRE(pos < gcode.size());
double z = 0;
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
REQUIRE_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_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));
}
// Verify that emit_machine_limits_to_gcode emits the correct max value across
// used extruders (regression for commit b4ee665: "Emit max value of machine
// limit among used extruders").
TEST_CASE("Machine envelope emits max limit among used extruders", "[PrintGCode][MachineEnvelope]")
{
SECTION("Single extruder emits its configured values") {
const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin2" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// stride-2 options: (normal, silent)
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "700,800" },
{ "machine_max_acceleration_z", "100,200" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
// stride-2 options: (normal, silent)
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("M201 uses the normal acceleration values") {
REQUIRE(gcode.find("M201 X500 Y700 Z100 E5000") != std::string::npos);
}
THEN("M203 uses the speed values") {
REQUIRE(gcode.find("M203 X100 Y110 Z10 E50") != std::string::npos);
}
THEN("M204 (Marlin 2) uses extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P1200 R1400 T1600") != std::string::npos);
}
THEN("M205 uses the jerk values") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
THEN("M205 J uses the junction deviation") {
REQUIRE(gcode.find("M205 J0.020") != std::string::npos);
}
}
SECTION("Legacy Marlin flavor emits correct format") {
const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// All machine limits must be provided — defaults are empty vectors.
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "500,600" },
{ "machine_max_acceleration_z", "500,600" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("Legacy Marlin: M204 travel_acc = extruding_acc") {
// gcfMarlinLegacy uses extruding acc for travel too
REQUIRE(gcode.find("M204 P1200 R1400 T1200") != std::string::npos);
}
THEN("Legacy Marlin: M205 uses mm/sec format") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
}
SECTION("Multi extruder - max of used extruders is emitted") {
// Build config with 2 extruders that have *different* machine limits.
// Extruder 1 has higher values; the emitted G-code must use the max.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
// Print basics
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
// Print objects sequentially so each uses its own extruder without
// wipe-tower / tool-change complexity.
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
// filament_map maps filament slot index (1-based) → logical extruder ID (1-based).
// Default [1] maps everything to extruder 0. Need [1, 2] for two distinct extruders.
// fmmManual prevents auto-computation from overwriting the explicit mapping.
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
// flush_volumes_matrix must be filament_count^2 * heads_count entries.
// 2 filaments * 2 * 1 head = 4 entries (all zero — flush volumes not tested here).
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// Machine limits: extruder 0 low, extruder 1 high
// Stride-2 (normal, silent pairs): e0_n, e0_s, e1_n, e1_s
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({700, 0, 1100, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 300, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 8000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({1200, 0, 2200, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({1400, 0, 2400, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({1600, 0, 2600, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({8, 0, 12, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({9, 0, 13, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.6, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({5, 0, 10, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.05, 0}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(mesh(TestMesh::cube_20x20x20));
obj1->add_instance();
// obj1 uses default extruder=1 (0-based index 0)
auto* obj2 = model.add_object();
obj2->add_volume(mesh(TestMesh::cube_20x20x20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{},
ArrangeParams{scaled(min_object_distance(config))});
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
std::string gcode = Slic3r::Test::gcode(print);
THEN("M201 contains max (extruder 1's) acceleration values") {
REQUIRE(gcode.find("M201 X1000 Y1100 Z300 E8000") != std::string::npos);
}
THEN("M203 contains max speed values") {
REQUIRE(gcode.find("M203 X200 Y210 Z30 E80") != std::string::npos);
}
THEN("M204 contains max extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P2200 R2400 T2600") != std::string::npos);
}
THEN("M205 contains max jerk values") {
REQUIRE(gcode.find("M205 X12.00 Y13.00 Z0.60 E10.00") != std::string::npos);
}
THEN("M205 contains max m_max_junction_deviation ") {
REQUIRE(gcode.find("M205 J0.050") != std::string::npos);
}
}
}
// Verify that the EXTRUDER_LIMIT macro (GCodeWriter.cpp) correctly:
// 1) Uses the active extruder's specific limit when filament() is known.
// 2) Falls back to the maximum of all extruder limits when filament() is nullptr.
//
// These two behaviours were introduced in:
// - "Use per-extruder motion limit" (1ab34a7454)
// - "Use max limit when current extruder is unknown" (b7240ab1c6)
TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[PrintGCode][MachineEnvelope]")
{
// --- Build config with 2 extruders that have different machine limits ---
// Extruder 0: low limits
// Extruder 1: high limits
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders, 2 filaments
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// --- Machine limits (stride-2: e0_n, e0_s, e1_n, e1_s) ---
// Extruder 0 has LOW limits, Extruder 1 has HIGH limits.
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 5000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({500, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({600, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({700, 0, 2500, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({5, 0, 15, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({6, 0, 16, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.8, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({3, 0, 8, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.08, 0}));
// --- Print acceleration: 1500 mm/s² ---
// Exceeds extruder 0's limit (500) → should be clamped to 500.
// Does NOT exceed extruder 1's limit (2000) → passes through as 1500.
config.set_key_value("default_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("outer_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("inner_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("top_surface_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("initial_layer_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("travel_acceleration", new ConfigOptionFloats({1500, 1500}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(mesh(TestMesh::cube_20x20x20));
obj1->add_instance();
auto* obj2 = model.add_object();
obj2->add_volume(mesh(TestMesh::cube_20x20x20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))});
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
std::string gcode = Slic3r::Test::gcode(print);
SECTION("Preamble: max limit among used extruders") {
THEN("M201 uses max (extruder 1's) acceleration values") {
REQUIRE(gcode.find("M201 X1000 Y1000 Z200 E5000") != std::string::npos);
}
THEN("M204 uses max extruding/retracting/travel") {
REQUIRE(gcode.find("M204 P2000 R2000 T2500") != std::string::npos);
}
THEN("M205 uses max jerk values") {
REQUIRE(gcode.find("M205 X15.00 Y16.00 Z0.80 E8.00") != std::string::npos);
}
}
SECTION("Preamble: EXTRUDER_LIMIT falls back to max when no filament is active") {
// set_junction_deviation() is called during preamble with no active filament.
// EXTRUDER_LIMIT(m_max_junction_deviation) → filament() == nullptr → max of all (0.08).
THEN("M205 J uses max junction deviation") {
REQUIRE(gcode.find("M205 J0.080") != std::string::npos);
}
}
SECTION("Print: extruder 0 acceleration clamped to its specific limit") {
// Extruder 0 machine limit = 500. Print accel = 1500 > 500 → clamped to 500.
THEN("M204 P500 appears (extruder 0 clamped)") {
REQUIRE(gcode.find("M204 P500") != std::string::npos);
}
THEN("M204 T700 appears (extruder 0 travel clamped)") {
REQUIRE(gcode.find("M204 T700") != std::string::npos);
}
}
SECTION("Print: extruder 1 acceleration NOT clamped to extruder 0's limit") {
// Extruder 1 machine limit = 2000. Print accel = 1500 < 2000 → not clamped.
THEN("M204 P1500 appears (extruder 1 not clamped to 500)") {
REQUIRE(gcode.find("M204 P1500") != std::string::npos);
}
}
}

View File

@@ -3,17 +3,21 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp" #include "test_helpers.hpp"
#include <iterator>
#include <set>
using namespace Slic3r; using namespace Slic3r;
using namespace Slic3r::Test; using namespace Slic3r::Test;
SCENARIO("PrintObject: object layer heights", "[PrintObject]") { SCENARIO("Object layer heights", "[PrintObject]") {
GIVEN("A 20mm cube") { GIVEN("A 20mm cube") {
WHEN("sliced with a 2mm layer height and a 3mm nozzle") { WHEN("sliced with a 2mm layer height and a 3mm nozzle") {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 }, { "initial_layer_print_height", 2 },
{ "layer_height", 2 }, { "layer_height", 2 },
{ "nozzle_diameter", 3 } { "nozzle_diameter", 3 }
@@ -32,7 +36,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
} }
WHEN("sliced with a 10mm layer height and an 11mm nozzle") { WHEN("sliced with a 10mm layer height and an 11mm nozzle") {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 }, { "initial_layer_print_height", 2 },
{ "layer_height", 10 }, { "layer_height", 10 },
{ "nozzle_diameter", 11 } { "nozzle_diameter", 11 }
@@ -50,7 +54,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
} }
WHEN("sliced with a 15mm layer height and a 16mm nozzle") { WHEN("sliced with a 15mm layer height and a 16mm nozzle") {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 }, { "initial_layer_print_height", 2 },
{ "layer_height", 15 }, { "layer_height", 15 },
{ "nozzle_diameter", 16 } { "nozzle_diameter", 16 }
@@ -71,7 +75,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
// rejects the slice during flow computation. Pin that behavior. // rejects the slice during flow computation. Pin that behavior.
THEN("Slicing is rejected") { THEN("Slicing is rejected") {
Slic3r::Print print; Slic3r::Print print;
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { REQUIRE_THROWS(Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 0.3 }, { "initial_layer_print_height", 0.3 },
{ "layer_height", 0.5 }, { "layer_height", 0.5 },
{ "nozzle_diameter", 0.4 } { "nozzle_diameter", 0.4 }
@@ -81,11 +85,11 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
} }
} }
SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") { SCENARIO("Perimeter generation", "[PrintObject]") {
GIVEN("20mm cube and default config") { GIVEN("20mm cube and default config") {
WHEN("make_perimeters() is called") { WHEN("make_perimeters() is called") {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { { "sparse_infill_density", 0 } }); Slic3r::Test::init_and_process_print({cube(20)}, print, { { "sparse_infill_density", 0 } });
const PrintObject &object = *print.objects().front(); const PrintObject &object = *print.objects().front();
THEN("Every layer in region 0 has 1 island of perimeters") { THEN("Every layer in region 0 has 1 island of perimeters") {
for (const Layer *layer : object.layers()) for (const Layer *layer : object.layers())
@@ -94,7 +98,7 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
} }
WHEN("wall_loops is set to 3") { WHEN("wall_loops is set to 3") {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "sparse_infill_density", 0 }, { "sparse_infill_density", 0 },
{ "wall_loops", 3 } { "wall_loops", 3 }
}); });
@@ -106,3 +110,23 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
} }
} }
} }
TEST_CASE("Initial layer height is honored", "[PrintObject]")
{
const std::string gcode = Slic3r::Test::slice({cube(20)}, {
{ "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));
}

View File

@@ -9,190 +9,276 @@
#include <cmath> #include <cmath>
#include "test_data.hpp" // get access to init_print, etc #include "test_helpers.hpp" // get access to init_print, etc
using namespace Slic3r::Test; using namespace Slic3r::Test;
using namespace Slic3r; using namespace Slic3r;
/// Helper method to find the tool used for the brim (always the first extrusion). // Distinct brim regions (combine_brims merges touching brims into one covering >1 object).
[[maybe_unused]] static int get_brim_tool(const std::string &gcode) static int brim_count(const Print &print)
{ {
int brim_tool = -1; int n = 0;
int tool = -1; for (const auto &group : print.skirt_brim_groups())
GCodeReader parser; n += (int) group.brims.size();
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line) return n;
{ }
// if the command is a T command, set the current tool
if (boost::starts_with(line.cmd(), "T")) { // Total brim loops across all objects.
tool = atoi(line.cmd().data() + 1); static size_t brim_loop_count(Print &print)
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) { {
brim_tool = tool; size_t n = 0;
for (const auto &kv : print.get_brimMap())
n += kv.second.items_count();
return n;
}
// The span is skirt_height layers, or every layer when a draft shield is on (forced even at
// height 0); per-object skirts are rejected in By object printing (no room between objects).
TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")
{
const int object_layers = 100; // 20mm cube at 0.2mm layers
const char *skirt_type = GENERATE("combined", "perobject");
const char *print_seq = GENERATE("by layer", "by object");
const char *draft_shield = GENERATE("disabled", "enabled");
const int skirt_height = GENERATE(0, 1, 3);
DYNAMIC_SECTION(skirt_type << " | " << print_seq << " | draft=" << draft_shield << " | height=" << skirt_height) {
auto do_slice = [&] {
return slice_two_cubes_arranged({
{ "skirt_loops", 1 },
{ "skirt_height", skirt_height },
{ "skirt_distance", 3 },
{ "skirt_type", skirt_type },
{ "draft_shield", draft_shield },
{ "print_sequence", print_seq },
{ "layer_height", 0.2 },
});
};
const bool draft = std::string(draft_shield) == "enabled";
const bool has_skirt = draft || skirt_height > 0;
const bool unsafe_by_object = std::string(skirt_type) == "perobject"
&& std::string(print_seq) == "by object" && has_skirt;
if (unsafe_by_object) {
REQUIRE_THROWS(do_slice());
} else {
const int expected_layers = draft ? object_layers : skirt_height;
CHECK(role_passes(do_slice(), "skirt") == expected_layers);
} }
}
}
// Each per-object skirt prints right before its own object, so distant objects yield two
// non-contiguous skirt passes; close objects group into a single skirt.
TEST_CASE("Per-object skirts group when objects are close", "[SkirtBrim]")
{
auto [gap, expected_skirts] = GENERATE(table<double, int>({ { 5.0, 1 }, { 60.0, 2 } }));
DYNAMIC_SECTION("gap=" << gap) {
const std::string gcode = slice_two_cubes_apart(gap, {
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "layer_height", 0.2 },
});
CHECK(role_passes(gcode, "skirt") == expected_skirts);
}
}
TEST_CASE("Combine brims merges touching brims", "[SkirtBrim]")
{
auto [gap, combine, expected_brims] = GENERATE(table<double, int, int>({
{ 5.0, 1, 1 }, // touching + combine -> one merged brim
{ 5.0, 0, 2 }, // touching, no combine -> separate
{ 60.0, 1, 2 }, // far apart -> nothing to merge
}));
DYNAMIC_SECTION("gap=" << gap << " combine_brims=" << combine) {
Print print;
Model model;
place_two_cubes_apart(gap, {
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
{ "combine_brims", combine },
{ "layer_height", 0.2 },
}, print, model);
print.process();
CHECK(brim_count(print) == expected_brims);
}
}
// Each object's skirt and brim come right before that object, not all skirts then all brims first.
TEST_CASE("By-layer per-object skirt and brim precede each object", "[SkirtBrim]")
{
const std::string gcode = slice_two_cubes_apart(60, { // far apart: a skirt+brim per object
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
{ "layer_height", 0.2 },
}); });
return brim_tool; const std::vector<std::string> expected{ "skirt", "brim", "perimeter", "skirt", "brim", "perimeter" };
CHECK(role_sequence(gcode, { "skirt", "brim", "perimeter" }) == expected);
}
// A square's corners are 90 degrees, so they get ears only when brim_ears_max_angle is above 90.
TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]")
{
auto [max_angle, expect_ears] = GENERATE(table<int, bool>({ { 91, true }, { 90, false }, { 89, false } }));
DYNAMIC_SECTION("brim_ears_max_angle=" << max_angle) {
Print print;
init_and_process_print({ cube(20) }, print, {
{ "skirt_loops", 0 },
{ "brim_type", "brim_ears" },
{ "brim_width", 1 },
{ "brim_ears_max_angle", max_angle },
{ "initial_layer_line_width", 0.5 },
});
if (expect_ears) CHECK(brim_loop_count(print) > 0);
else CHECK(brim_loop_count(print) == 0);
}
}
SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") {
GIVEN("20mm cube and default config") {
WHEN("skirt_loops is set to 2") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "skirt_height", 1 },
{ "skirt_distance", 1 },
{ "skirt_loops", 2 }
});
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
SCENARIO("Brim has the configured number of loops", "[SkirtBrim]") {
GIVEN("20mm cube and default config, 1mm first layer width") {
WHEN("Brim is set to 6mm") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "brim_type", "outer_only" },
{ "initial_layer_line_width", 1 },
{ "brim_width", 6 }
});
THEN("Brim Extrusion collection has 6 loops in it") {
REQUIRE(brim_loop_count(print) == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "brim_type", "outer_only" },
{ "brim_width", 6 },
{ "initial_layer_line_width", 0.5 }
});
THEN("Brim Extrusion collection has 12 loops in it") {
REQUIRE(brim_loop_count(print) == 12);
}
}
}
} }
TEST_CASE("Skirt height is honored", "[SkirtBrim]") { TEST_CASE("Skirt height is honored", "[SkirtBrim]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "skirt_loops", 1 }, { "skirt_loops", 1 },
{ "skirt_height", 5 }, { "skirt_height", 5 },
{ "wall_loops", 0 }, { "wall_loops", 0 },
{ "gcode_comments", true }
}); });
std::string gcode; std::string gcode;
SECTION("printing a single object") { SECTION("printing a single object") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); gcode = slice({ cube(20) }, config);
} }
SECTION("printing multiple objects") { SECTION("printing multiple objects") {
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20, TestMesh::cube_20x20x20}, config); gcode = slice({ cube(20), cube(20) }, config);
} }
REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t)config.opt_int("skirt_height")); REQUIRE(layers_with_role(gcode, "skirt").size() == (size_t) config.opt_int("skirt_height"));
} }
SCENARIO("Skirt and brim generation", "[SkirtBrim]") { SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
GIVEN("A default configuration") { GIVEN("A default configuration") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_num_extruders(4); config.set_num_extruders(4);
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "initial_layer_print_height", 0.3 }, { "initial_layer_print_height", 0.3 },
{ "gcode_comments", true }, // avoid altering speeds unexpectedly
// avoid altering speeds unexpectedly
{ "slow_down_for_layer_cooling", false }, { "slow_down_for_layer_cooling", false },
{ "initial_layer_speed", "100%" }, { "initial_layer_speed", "100%" },
// remove noise from top/solid layers // remove noise from top/solid layers
{ "top_shell_layers", 0 }, { "top_shell_layers", 0 },
{ "bottom_shell_layers", 1 }, { "bottom_shell_layers", 1 },
{ "machine_start_gcode", "T[initial_tool]\n" } { "machine_start_gcode", "T[initial_tool]\n" },
}); });
WHEN("Brim width is set to 5") { WHEN("Brim width is set to 5") {
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "wall_loops", 0 }, { "wall_loops", 0 },
{ "skirt_loops", 0 }, { "skirt_loops", 0 },
{ "brim_type", "outer_only" }, { "brim_type", "outer_only" },
{ "brim_width", 5 } { "brim_width", 5 },
}); });
THEN("Brim is generated") { THEN("Brim is generated") {
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); std::string gcode = slice({ cube(20) }, config);
REQUIRE(! layers_with_role(gcode, "brim").empty()); REQUIRE(! layers_with_role(gcode, "brim").empty());
} }
} }
#if 0
// This is a real error! One shall print the brim with the external perimeter extruder!
WHEN("Perimeter extruder = 2 and support extruders = 3") {
THEN("Brim is printed with the extruder used for the perimeters of first object") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("perimeter_extruder") - 1);
}
}
WHEN("Perimeter extruder = 2, support extruders = 3, raft is enabled") {
THEN("brim is printed with same extruder as skirt") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "brim_width", 5 },
{ "perimeter_extruder", 2 },
{ "support_material_extruder", 3 },
{ "infill_extruder", 4 },
{ "raft_layers", 1 }
});
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
int tool = get_brim_tool(gcode);
REQUIRE(tool == config.opt_int("support_material_extruder") - 1);
}
}
#endif
WHEN("brim width to 1 with layer_width of 0.5") { WHEN("brim width to 1 with layer_width of 0.5") {
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "skirt_loops", 0 }, { "skirt_loops", 0 },
{ "initial_layer_line_width", 0.5 }, { "initial_layer_line_width", 0.5 },
{ "brim_type", "outer_only" }, { "brim_type", "outer_only" },
{ "brim_width", 1 } { "brim_width", 1 },
}); });
THEN("2 brim lines") { THEN("2 brim lines") {
Slic3r::Print print; Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config); init_and_process_print({ cube(20) }, print, config);
size_t total_entities = 0; REQUIRE(brim_loop_count(print) == 2);
for (const auto& pair : print.get_brimMap()) {
total_entities += pair.second.entities.size();
}
REQUIRE(total_entities == 2);
} }
} }
#if 0
WHEN("brim ears on a square") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 91 }
});
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
THEN("Four brim ears") {
REQUIRE(print.brim().entities.size() == 4);
}
}
WHEN("brim ears on a square but with a too small max angle") {
config.set_deserialize_strict({
{ "skirts", 0 },
{ "first_layer_extrusion_width", 0.5 },
{ "brim_width", 1 },
{ "brim_ears", 1 },
{ "brim_ears_max_angle", 89 }
});
THEN("no brim") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, config);
REQUIRE(print.brim().entities.size() == 0);
}
}
#endif
WHEN("Object is plated with overhang support and a brim") { WHEN("Object is plated with overhang support and a brim") {
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "layer_height", 0.4 }, { "layer_height", 0.4 },
{ "initial_layer_print_height", 0.4 }, { "initial_layer_print_height", 0.4 },
{ "skirt_loops", 1 }, { "skirt_loops", 1 },
{ "skirt_distance", 0 }, { "skirt_distance", 0 },
{ "enable_support", 1 }, { "enable_support", 1 },
{ "brim_type", "outer_only" }, { "brim_type", "outer_only" },
{ "brim_width", 5 } { "brim_width", 5 },
}); });
THEN("Support and brim are both emitted") { THEN("Support and brim are both emitted") {
std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); std::string gcode = slice({ TestMesh::overhang }, config);
REQUIRE(! layers_with_role(gcode, "support").empty()); REQUIRE(! layers_with_role(gcode, "support").empty());
REQUIRE(! layers_with_role(gcode, "brim").empty()); REQUIRE(! layers_with_role(gcode, "brim").empty());
} }
} }
WHEN("an object with support is surrounded by a skirt") { WHEN("an object with support is surrounded by a skirt") {
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "enable_support", 1 }, { "enable_support", 1 },
{ "skirt_loops", 1 }, { "skirt_loops", 1 },
{ "skirt_distance", 2 }, { "skirt_distance", 2 },
{ "brim_type", "no_brim" }, { "brim_type", "no_brim" },
{ "z_hop", 0 } { "z_hop", 0 },
}); });
THEN("the skirt is long enough to enclose the object and its support") { THEN("the skirt is long enough to enclose the object and its support") {
std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config); std::string gcode = slice({ TestMesh::overhang }, config);
const double first_layer_z = config.opt_float("initial_layer_print_height"); const double first_layer_z = config.opt_float("initial_layer_print_height");
// On the first layer, accumulate the skirt loop length and collect the // On the first layer, accumulate the skirt loop length and collect the
@@ -200,7 +286,7 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
double skirt_length = 0.0; double skirt_length = 0.0;
Points footprint; Points footprint;
GCodeReader parser; GCodeReader parser;
parser.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) { 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) if (! line.extruding(self) || line.dist_XY(self) <= 0 || std::abs(self.z() - first_layer_z) > 0.01)
return; return;
if (line.comment().find("skirt") != std::string_view::npos) if (line.comment().find("skirt") != std::string_view::npos)
@@ -214,17 +300,18 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
REQUIRE(skirt_length > hull_perimeter); REQUIRE(skirt_length > hull_perimeter);
} }
} }
WHEN("Large minimum skirt length is used.") { WHEN("Large minimum skirt length is used.") {
// One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops. // One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops.
config.set_deserialize_strict({ config.set_deserialize_strict({
{ "skirt_loops", 1 }, { "skirt_loops", 1 },
{ "min_skirt_length", 500 } { "min_skirt_length", 500 },
}); });
THEN("The skirt is extended to at least the minimum length") { THEN("The skirt is extended to at least the minimum length") {
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config); std::string gcode = slice({ cube(20) }, config);
double skirt_length = 0.0; double skirt_length = 0.0;
GCodeReader parser; GCodeReader parser;
parser.parse_buffer(gcode, [&skirt_length] (GCodeReader& self, const GCodeReader::GCodeLine& line) { parser.parse_buffer(gcode, [&skirt_length](GCodeReader &self, const GCodeReader::GCodeLine &line) {
if (line.extruding(self) && line.comment().find("skirt") != std::string_view::npos) if (line.extruding(self) && line.comment().find("skirt") != std::string_view::npos)
skirt_length += line.dist_XY(self); skirt_length += line.dist_XY(self);
}); });

View File

@@ -3,22 +3,22 @@
#include "libslic3r/GCodeReader.hpp" #include "libslic3r/GCodeReader.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "test_data.hpp" // get access to init_print, etc #include "test_helpers.hpp" // get access to init_print, etc
using namespace Slic3r::Test; using namespace Slic3r::Test;
using namespace Slic3r; using namespace Slic3r;
TEST_CASE("SupportMaterial: Three raft layers created", "[SupportMaterial]") TEST_CASE("Three raft layers are created", "[SupportMaterial]")
{ {
Slic3r::Print print; Slic3r::Print print;
Slic3r::Test::init_and_process_print({ TestMesh::cube_20x20x20 }, print, { Slic3r::Test::init_and_process_print({ cube(20) }, print, {
{ "enable_support", 1 }, { "enable_support", 1 },
{ "raft_layers", 3 } { "raft_layers", 3 }
}); });
REQUIRE(print.objects().front()->support_layers().size() == 3); REQUIRE(print.objects().front()->support_layers().size() == 3);
} }
TEST_CASE("SupportMaterial: enforced support layers are generated", "[SupportMaterial]") TEST_CASE("Enforced support layers are generated", "[SupportMaterial]")
{ {
// enforce_support_layers forces support on the first N layers even with support off. // enforce_support_layers forces support on the first N layers even with support off.
Slic3r::Print baseline; Slic3r::Print baseline;
@@ -36,7 +36,7 @@ TEST_CASE("SupportMaterial: enforced support layers are generated", "[SupportMat
REQUIRE(enforced.objects().front()->support_layers().size() > 0); REQUIRE(enforced.objects().front()->support_layers().size() > 0);
} }
SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMaterial]") SCENARIO("Support layer Z honors contact distance", "[SupportMaterial]")
{ {
// Box h = 20mm, hole bottom at 5mm, hole height 10mm (top edge at 15mm). // 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); TriangleMesh mesh = Slic3r::Test::mesh(Slic3r::Test::TestMesh::cube_with_hole);

View File

@@ -12,14 +12,14 @@
#include <chrono> #include <chrono>
//#include "test_options.hpp" //#include "test_options.hpp"
#include "test_data.hpp" #include "test_helpers.hpp"
using namespace Slic3r; using namespace Slic3r;
using namespace std; using namespace std;
static inline TriangleMesh make_cube() { return make_cube(20., 20, 20); } static inline TriangleMesh make_cube() { return make_cube(20., 20, 20); }
SCENARIO( "TriangleMesh: Basic mesh statistics") { SCENARIO("Basic mesh statistics", "[TriangleMesh]") {
GIVEN( "A 20mm cube, built from constexpr std::array" ) { GIVEN( "A 20mm cube, built from constexpr std::array" ) {
std::vector<Vec3f> vertices { {20,20,0}, {20,0,0}, {0,0,0}, {0,20,0}, {20,20,20}, {0,20,20}, {0,0,20}, {20,0,20} }; std::vector<Vec3f> vertices { {20,20,0}, {20,0,0}, {0,0,0}, {0,20,0}, {20,20,20}, {0,20,20}, {0,0,20}, {20,0,20} };
std::vector<Vec3i32> facets { {0,1,2}, {0,2,3}, {4,5,6}, {4,6,7}, {0,4,7}, {0,7,1}, {1,7,6}, {1,6,2}, {2,6,5}, {2,5,3}, {4,0,3}, {4,3,5} }; std::vector<Vec3i32> facets { {0,1,2}, {0,2,3}, {4,5,6}, {4,6,7}, {0,4,7}, {0,7,1}, {1,7,6}, {1,6,2}, {2,6,5}, {2,5,3}, {4,0,3}, {4,3,5} };
@@ -71,7 +71,7 @@ SCENARIO( "TriangleMesh: Basic mesh statistics") {
} }
} }
SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") { SCENARIO("Transformation functions affect the mesh as expected", "[TriangleMesh]") {
GIVEN( "A 20mm cube with one corner on the origin") { GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube(); auto cube = make_cube();
@@ -134,7 +134,7 @@ SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") {
} }
} }
SCENARIO( "TriangleMesh: slice behavior.") { SCENARIO("Slice behavior", "[TriangleMesh]") {
GIVEN( "A 20mm cube with one corner on the origin") { GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube(); auto cube = make_cube();
@@ -177,7 +177,7 @@ SCENARIO( "TriangleMesh: slice behavior.") {
} }
} }
SCENARIO( "make_xxx functions produce meshes.") { SCENARIO("make_xxx functions produce meshes", "[TriangleMesh]") {
GIVEN("make_cube() function") { GIVEN("make_cube() function") {
WHEN("make_cube() is called with arguments 20,20,20") { WHEN("make_cube() is called with arguments 20,20,20") {
TriangleMesh cube = make_cube(20,20,20); TriangleMesh cube = make_cube(20,20,20);
@@ -232,7 +232,7 @@ SCENARIO( "make_xxx functions produce meshes.") {
} }
} }
SCENARIO( "TriangleMesh: split functionality.") { SCENARIO("Split functionality", "[TriangleMesh]") {
GIVEN( "A 20mm cube with one corner on the origin") { GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube(); auto cube = make_cube();
WHEN( "The mesh is split into its component parts.") { WHEN( "The mesh is split into its component parts.") {
@@ -260,7 +260,7 @@ SCENARIO( "TriangleMesh: split functionality.") {
} }
} }
SCENARIO( "TriangleMesh: Mesh merge functions") { SCENARIO("Mesh merge functions", "[TriangleMesh]") {
GIVEN( "Two 20mm cubes, each with one corner on the origin") { GIVEN( "Two 20mm cubes, each with one corner on the origin") {
auto cube = make_cube(); auto cube = make_cube();
TriangleMesh cube2(cube); TriangleMesh cube2(cube);
@@ -274,7 +274,7 @@ SCENARIO( "TriangleMesh: Mesh merge functions") {
} }
} }
SCENARIO( "TriangleMeshSlicer: Cut behavior.") { SCENARIO("Cut behavior", "[TriangleMesh]") {
GIVEN( "A 20mm cube with one corner on the origin") { GIVEN( "A 20mm cube with one corner on the origin") {
auto cube = make_cube(); auto cube = make_cube();
WHEN( "Object is cut at the bottom") { WHEN( "Object is cut at the bottom") {
@@ -302,7 +302,7 @@ SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
} }
} }
#ifdef TEST_PERFORMANCE #ifdef TEST_PERFORMANCE
TEST_CASE("Regression test for issue #4486 - files take forever to slice") { TEST_CASE("Large mesh slices within the time budget (#4486)", "[TriangleMesh][Regression]") {
TriangleMesh mesh; TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl"); mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl");
@@ -329,7 +329,7 @@ TEST_CASE("Regression test for issue #4486 - files take forever to slice") {
#endif // TEST_PERFORMANCE #endif // TEST_PERFORMANCE
#ifdef BUILD_PROFILE #ifdef BUILD_PROFILE
TEST_CASE("Profile test for issue #4486 - files take forever to slice") { TEST_CASE("Large mesh slicing profile (#4486)", "[TriangleMesh][Profile]") {
TriangleMesh mesh; TriangleMesh mesh;
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config(); DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl"); mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl");