mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-18 00:12:09 +00:00
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:
@@ -1154,7 +1154,7 @@ private:
|
||||
PrintRegionPtrs m_print_regions;
|
||||
|
||||
//SoftFever
|
||||
bool m_isBBLPrinter;
|
||||
bool m_isBBLPrinter = false;
|
||||
|
||||
// Ordered collections of extrusion paths to build skirt loops and brim.
|
||||
ExtrusionEntityCollection m_skirt;
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
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
|
||||
|
||||
### 1. **SECTIONS IN LOOPS - NEVER REUSE NAMES**
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests.cpp
|
||||
test_data.cpp
|
||||
test_data.hpp
|
||||
test_helpers.cpp
|
||||
test_helpers.hpp
|
||||
test_cooling.cpp
|
||||
test_extrusion_entity.cpp
|
||||
test_fill.cpp
|
||||
test_flow.cpp
|
||||
test_gcode.cpp
|
||||
test_gcode_timing.cpp
|
||||
test_gcodewriter.cpp
|
||||
test_model.cpp
|
||||
test_multifilament.cpp
|
||||
test_print.cpp
|
||||
test_printgcode.cpp
|
||||
test_printobject.cpp
|
||||
test_skirt_brim.cpp
|
||||
test_support_material.cpp
|
||||
|
||||
93
tests/fff_print/README.md
Normal file
93
tests/fff_print/README.md
Normal 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]"
|
||||
27
tests/fff_print/test_cooling.cpp
Normal file
27
tests/fff_print/test_cooling.cpp
Normal 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);
|
||||
}
|
||||
@@ -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
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -35,7 +35,7 @@ static Slic3r::ExtrusionPaths random_paths(size_t count = 10, size_t length = 20
|
||||
return p;
|
||||
}
|
||||
|
||||
SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]") {
|
||||
SCENARIO("Polygon flattening", "[ExtrusionEntity]") {
|
||||
srand(0xDEADBEEF); // consistent seed for test reproducibility.
|
||||
|
||||
// Generate one specific random path set and save it for later comparison
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
|
||||
|
||||
#if 0
|
||||
TEST_CASE("Fill: adjusted solid distance") {
|
||||
TEST_CASE("Adjusted solid distance", "[Fill]") {
|
||||
int surface_width = 250;
|
||||
int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
|
||||
REQUIRE(distance == Catch::Approx(50));
|
||||
@@ -26,7 +26,7 @@ TEST_CASE("Fill: adjusted solid distance") {
|
||||
}
|
||||
#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"));
|
||||
filler->angle = float(-(PI)/2.0);
|
||||
FillParams fill_params;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <numeric>
|
||||
#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/Model.hpp"
|
||||
@@ -17,7 +17,7 @@ using namespace Slic3r;
|
||||
|
||||
/// Test the expected behavior for auto-width,
|
||||
/// 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") {
|
||||
ConfigOptionFloatOrPercent width(1.0, false);
|
||||
float nozzle_diameter = 0.4f;
|
||||
@@ -79,7 +79,7 @@ SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
|
||||
}
|
||||
|
||||
/// 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") {
|
||||
float nozzle_diameter = 0.4f;
|
||||
float bridge_flow = 1.0f;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,15 @@
|
||||
#include <memory>
|
||||
|
||||
#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::Test;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
@@ -19,13 +19,11 @@ using namespace std;
|
||||
|
||||
namespace Slic3r { namespace Test {
|
||||
|
||||
// Mesh enumeration to name mapping
|
||||
const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names {
|
||||
std::pair<TestMesh, const char*>(TestMesh::A, "A"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::L, "L"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::V, "V"),
|
||||
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::bridge, "bridge"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole, "bridge_with_hole"),
|
||||
@@ -46,9 +44,6 @@ TriangleMesh mesh(TestMesh m)
|
||||
{
|
||||
TriangleMesh mesh;
|
||||
switch(m) {
|
||||
case TestMesh::cube_20x20x20:
|
||||
mesh = Slic3r::make_cube(20, 20, 20);
|
||||
break;
|
||||
case TestMesh::sphere_50mm:
|
||||
mesh = Slic3r::make_sphere(50, PI / 243.0);
|
||||
break;
|
||||
@@ -187,30 +182,72 @@ TriangleMesh mesh(TestMesh m)
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static bool verbose_gcode()
|
||||
Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh)
|
||||
{
|
||||
const char *v = std::getenv("SLIC3R_TESTS_GCODE");
|
||||
if (v == nullptr)
|
||||
return false;
|
||||
std::string s(v);
|
||||
return s == "1" || s == "on" || s == "yes";
|
||||
Slic3r::Model result;
|
||||
ModelObject *object = result.add_object();
|
||||
object->name += model_name + ".stl";
|
||||
object->add_volume(_mesh);
|
||||
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();
|
||||
config.apply(config_in);
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
|
||||
if (verbose_gcode())
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
|
||||
size_t object_idx = 0;
|
||||
for (const TriangleMesh &t : meshes) {
|
||||
ModelObject *object = model.add_object();
|
||||
object->name += "object.stl";
|
||||
object->add_volume(std::move(t));
|
||||
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) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
@@ -221,63 +258,63 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
|
||||
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;
|
||||
triangle_meshes.reserve(test_meshes.size());
|
||||
for (const TestMesh test_mesh : test_meshes)
|
||||
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;
|
||||
triangle_meshes.reserve(input_meshes.size());
|
||||
for (const TriangleMesh &input_mesh : input_meshes)
|
||||
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();
|
||||
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();
|
||||
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;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
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;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
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;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
init_print(meshes, print, model, config_items);
|
||||
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;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
init_print(meshes, print, model, config_items);
|
||||
print.process();
|
||||
}
|
||||
|
||||
@@ -292,6 +329,76 @@ std::string gcode(Print & print)
|
||||
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;
|
||||
@@ -313,59 +420,48 @@ double max_z(const std::string &gcode)
|
||||
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;
|
||||
ModelObject *object = result.add_object();
|
||||
object->name += model_name + ".stl";
|
||||
object->add_volume(_mesh);
|
||||
object->add_instance();
|
||||
return result;
|
||||
int passes = 0;
|
||||
bool in_role = false;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
if (! line.extruding(self)) return;
|
||||
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;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
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);
|
||||
std::vector<std::string> seq;
|
||||
std::string current;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
if (! line.extruding(self)) return;
|
||||
const std::string_view comment = line.comment();
|
||||
for (const std::string &role : roles)
|
||||
if (comment.find(role) != std::string_view::npos) {
|
||||
if (current != role) { seq.push_back(role); current = role; }
|
||||
break;
|
||||
}
|
||||
});
|
||||
return seq;
|
||||
}
|
||||
|
||||
} } // namespace Slic3r::Test
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
SCENARIO("init_print functionality", "[test_data]") {
|
||||
SCENARIO("init_print functionality", "[test_helpers]") {
|
||||
GIVEN("A default config") {
|
||||
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
WHEN("init_print is called with a single mesh.") {
|
||||
Slic3r::Model model;
|
||||
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.") {
|
||||
REQUIRE(print.objects().size() == 1);
|
||||
}
|
||||
125
tests/fff_print/test_helpers.hpp
Normal file
125
tests/fff_print/test_helpers.hpp
Normal 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
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
87
tests/fff_print/test_multifilament.cpp
Normal file
87
tests/fff_print/test_multifilament.cpp
Normal 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
|
||||
}
|
||||
@@ -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 "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
SCENARIO("Print: Skirt generation", "[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]") {
|
||||
SCENARIO("Changing the number of solid shell layers does not make all surfaces internal", "[Print]") {
|
||||
GIVEN("sliced 20mm cube and config with top_shell_layers = 2 and bottom_shell_layers = 1") {
|
||||
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
@@ -40,7 +37,7 @@ SCENARIO("Print: Changing number of solid shell layers does not cause all surfac
|
||||
});
|
||||
Slic3r::Print print;
|
||||
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)
|
||||
// and one solid bottom layer (0).
|
||||
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
|
||||
//
|
||||
@@ -129,7 +91,7 @@ void build_cubes(Slic3r::Model& model, Slic3r::Print& print,
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
ModelObject* object = model.add_object();
|
||||
object->add_volume(Slic3r::Test::mesh(TestMesh::cube_20x20x20));
|
||||
object->add_volume(cube(20));
|
||||
ModelInstance* inst = object->add_instance();
|
||||
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
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,21 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Print.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::Test;
|
||||
|
||||
SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
SCENARIO("Object layer heights", "[PrintObject]") {
|
||||
GIVEN("A 20mm cube") {
|
||||
WHEN("sliced with a 2mm layer height and a 3mm nozzle") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 2 },
|
||||
{ "layer_height", 2 },
|
||||
{ "nozzle_diameter", 3 }
|
||||
@@ -32,7 +36,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
}
|
||||
WHEN("sliced with a 10mm layer height and an 11mm nozzle") {
|
||||
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 },
|
||||
{ "layer_height", 10 },
|
||||
{ "nozzle_diameter", 11 }
|
||||
@@ -50,7 +54,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
}
|
||||
WHEN("sliced with a 15mm layer height and a 16mm nozzle") {
|
||||
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 },
|
||||
{ "layer_height", 15 },
|
||||
{ "nozzle_diameter", 16 }
|
||||
@@ -71,7 +75,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
// rejects the slice during flow computation. Pin that behavior.
|
||||
THEN("Slicing is rejected") {
|
||||
Slic3r::Print print;
|
||||
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "layer_height", 0.5 },
|
||||
{ "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") {
|
||||
WHEN("make_perimeters() is called") {
|
||||
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();
|
||||
THEN("Every layer in region 0 has 1 island of perimeters") {
|
||||
for (const Layer *layer : object.layers())
|
||||
@@ -94,7 +98,7 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
|
||||
}
|
||||
WHEN("wall_loops is set to 3") {
|
||||
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 },
|
||||
{ "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));
|
||||
}
|
||||
|
||||
@@ -9,190 +9,276 @@
|
||||
|
||||
#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;
|
||||
|
||||
/// Helper method to find the tool used for the brim (always the first extrusion).
|
||||
[[maybe_unused]] static int get_brim_tool(const std::string &gcode)
|
||||
// Distinct brim regions (combine_brims merges touching brims into one covering >1 object).
|
||||
static int brim_count(const Print &print)
|
||||
{
|
||||
int brim_tool = -1;
|
||||
int tool = -1;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
|
||||
{
|
||||
// if the command is a T command, set the current tool
|
||||
if (boost::starts_with(line.cmd(), "T")) {
|
||||
tool = atoi(line.cmd().data() + 1);
|
||||
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) {
|
||||
brim_tool = tool;
|
||||
int n = 0;
|
||||
for (const auto &group : print.skirt_brim_groups())
|
||||
n += (int) group.brims.size();
|
||||
return n;
|
||||
}
|
||||
|
||||
// Total brim loops across all objects.
|
||||
static size_t brim_loop_count(Print &print)
|
||||
{
|
||||
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]") {
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "skirt_loops", 1 },
|
||||
{ "skirt_height", 5 },
|
||||
{ "wall_loops", 0 },
|
||||
{ "gcode_comments", true }
|
||||
{ "skirt_loops", 1 },
|
||||
{ "skirt_height", 5 },
|
||||
{ "wall_loops", 0 },
|
||||
});
|
||||
|
||||
std::string gcode;
|
||||
std::string gcode;
|
||||
SECTION("printing a single object") {
|
||||
gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
|
||||
gcode = slice({ cube(20) }, config);
|
||||
}
|
||||
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]") {
|
||||
GIVEN("A default configuration") {
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_num_extruders(4);
|
||||
config.set_deserialize_strict({
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_num_extruders(4);
|
||||
config.set_deserialize_strict({
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "gcode_comments", true },
|
||||
// avoid altering speeds unexpectedly
|
||||
// avoid altering speeds unexpectedly
|
||||
{ "slow_down_for_layer_cooling", false },
|
||||
{ "initial_layer_speed", "100%" },
|
||||
// remove noise from top/solid layers
|
||||
// remove noise from top/solid layers
|
||||
{ "top_shell_layers", 0 },
|
||||
{ "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") {
|
||||
config.set_deserialize_strict({
|
||||
config.set_deserialize_strict({
|
||||
{ "wall_loops", 0 },
|
||||
{ "skirt_loops", 0 },
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 5 }
|
||||
});
|
||||
THEN("Brim is generated") {
|
||||
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
|
||||
{ "brim_width", 5 },
|
||||
});
|
||||
THEN("Brim is generated") {
|
||||
std::string gcode = slice({ cube(20) }, config);
|
||||
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") {
|
||||
config.set_deserialize_strict({
|
||||
config.set_deserialize_strict({
|
||||
{ "skirt_loops", 0 },
|
||||
{ "initial_layer_line_width", 0.5 },
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 1 }
|
||||
});
|
||||
{ "brim_width", 1 },
|
||||
});
|
||||
THEN("2 brim lines") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, config);
|
||||
size_t total_entities = 0;
|
||||
for (const auto& pair : print.get_brimMap()) {
|
||||
total_entities += pair.second.entities.size();
|
||||
}
|
||||
REQUIRE(total_entities == 2);
|
||||
Print print;
|
||||
init_and_process_print({ cube(20) }, print, config);
|
||||
REQUIRE(brim_loop_count(print) == 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") {
|
||||
config.set_deserialize_strict({
|
||||
config.set_deserialize_strict({
|
||||
{ "layer_height", 0.4 },
|
||||
{ "initial_layer_print_height", 0.4 },
|
||||
{ "skirt_loops", 1 },
|
||||
{ "skirt_distance", 0 },
|
||||
{ "enable_support", 1 },
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 5 }
|
||||
});
|
||||
|
||||
{ "brim_width", 5 },
|
||||
});
|
||||
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, "brim").empty());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
WHEN("an object with support is surrounded by a skirt") {
|
||||
config.set_deserialize_strict({
|
||||
{ "enable_support", 1 },
|
||||
{ "skirt_loops", 1 },
|
||||
{ "skirt_distance", 2 },
|
||||
{ "brim_type", "no_brim" },
|
||||
{ "z_hop", 0 }
|
||||
{ "z_hop", 0 },
|
||||
});
|
||||
THEN("the skirt is long enough to enclose the object and its support") {
|
||||
std::string gcode = Slic3r::Test::slice({TestMesh::overhang}, config);
|
||||
std::string gcode = slice({ TestMesh::overhang }, config);
|
||||
const double first_layer_z = config.opt_float("initial_layer_print_height");
|
||||
|
||||
// On the first layer, accumulate the skirt loop length and collect the
|
||||
@@ -200,7 +286,7 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
|
||||
double skirt_length = 0.0;
|
||||
Points footprint;
|
||||
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)
|
||||
return;
|
||||
if (line.comment().find("skirt") != std::string_view::npos)
|
||||
@@ -214,17 +300,18 @@ SCENARIO("Skirt and brim generation", "[SkirtBrim]") {
|
||||
REQUIRE(skirt_length > hull_perimeter);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("Large minimum skirt length is used.") {
|
||||
// One skirt loop around a 20mm cube is ~88mm, so 500mm forces extra loops.
|
||||
config.set_deserialize_strict({
|
||||
{ "skirt_loops", 1 },
|
||||
{ "min_skirt_length", 500 }
|
||||
{ "min_skirt_length", 500 },
|
||||
});
|
||||
THEN("The skirt is extended to at least the minimum length") {
|
||||
std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, config);
|
||||
std::string gcode = slice({ cube(20) }, config);
|
||||
double skirt_length = 0.0;
|
||||
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)
|
||||
skirt_length += line.dist_XY(self);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,22 @@
|
||||
#include "libslic3r/GCodeReader.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_CASE("SupportMaterial: Three raft layers created", "[SupportMaterial]")
|
||||
TEST_CASE("Three raft layers are created", "[SupportMaterial]")
|
||||
{
|
||||
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 },
|
||||
{ "raft_layers", 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.
|
||||
Slic3r::Print baseline;
|
||||
@@ -36,7 +36,7 @@ TEST_CASE("SupportMaterial: enforced support layers are generated", "[SupportMat
|
||||
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).
|
||||
TriangleMesh mesh = Slic3r::Test::mesh(Slic3r::Test::TestMesh::cube_with_hole);
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
#include <chrono>
|
||||
|
||||
//#include "test_options.hpp"
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace std;
|
||||
|
||||
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" ) {
|
||||
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} };
|
||||
@@ -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") {
|
||||
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") {
|
||||
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") {
|
||||
WHEN("make_cube() is called with arguments 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") {
|
||||
auto cube = make_cube();
|
||||
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") {
|
||||
auto cube = make_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") {
|
||||
auto cube = make_cube();
|
||||
WHEN( "Object is cut at the bottom") {
|
||||
@@ -302,7 +302,7 @@ SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
|
||||
}
|
||||
}
|
||||
#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;
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
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
|
||||
|
||||
#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;
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl");
|
||||
|
||||
Reference in New Issue
Block a user