From 29f31b9b387482aa93acaaf1c54ef3598720d827 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Mon, 6 Jul 2026 09:24:24 -0500 Subject: [PATCH] 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. --- src/libslic3r/Print.hpp | 2 +- tests/CLAUDE.md | 5 + tests/fff_print/CMakeLists.txt | 8 +- tests/fff_print/README.md | 93 +++ tests/fff_print/test_cooling.cpp | 27 + tests/fff_print/test_data.hpp | 95 --- tests/fff_print/test_extrusion_entity.cpp | 4 +- tests/fff_print/test_fill.cpp | 6 +- tests/fff_print/test_flow.cpp | 6 +- tests/fff_print/test_gcode.cpp | 22 - tests/fff_print/test_gcodewriter.cpp | 347 ++++++++++ .../{test_data.cpp => test_helpers.cpp} | 238 +++++-- tests/fff_print/test_helpers.hpp | 125 ++++ tests/fff_print/test_model.cpp | 2 +- tests/fff_print/test_multifilament.cpp | 87 +++ tests/fff_print/test_print.cpp | 193 ++++-- tests/fff_print/test_printgcode.cpp | 629 ------------------ tests/fff_print/test_printobject.cpp | 42 +- tests/fff_print/test_skirt_brim.cpp | 333 ++++++---- tests/fff_print/test_support_material.cpp | 10 +- tests/fff_print/test_trianglemesh.cpp | 20 +- 21 files changed, 1260 insertions(+), 1034 deletions(-) create mode 100644 tests/fff_print/README.md create mode 100644 tests/fff_print/test_cooling.cpp delete mode 100644 tests/fff_print/test_data.hpp delete mode 100644 tests/fff_print/test_gcode.cpp rename tests/fff_print/{test_data.cpp => test_helpers.cpp} (94%) create mode 100644 tests/fff_print/test_helpers.hpp create mode 100644 tests/fff_print/test_multifilament.cpp delete mode 100644 tests/fff_print/test_printgcode.cpp diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index af144ee821..b9f70506fb 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -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; diff --git a/tests/CLAUDE.md b/tests/CLAUDE.md index 16714a710a..dcbeab7cbd 100644 --- a/tests/CLAUDE.md +++ b/tests/CLAUDE.md @@ -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** diff --git a/tests/fff_print/CMakeLists.txt b/tests/fff_print/CMakeLists.txt index a91d25609a..bdd94785c7 100644 --- a/tests/fff_print/CMakeLists.txt +++ b/tests/fff_print/CMakeLists.txt @@ -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 diff --git a/tests/fff_print/README.md b/tests/fff_print/README.md new file mode 100644 index 0000000000..c8ed85e367 --- /dev/null +++ b/tests/fff_print/README.md @@ -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_.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_.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_.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//fff_print_tests --order rand "~[NotWorking]" diff --git a/tests/fff_print/test_cooling.cpp b/tests/fff_print/test_cooling.cpp new file mode 100644 index 0000000000..ec29b5fdc9 --- /dev/null +++ b/tests/fff_print/test_cooling.cpp @@ -0,0 +1,27 @@ +#include + +#include "test_helpers.hpp" + +#include + +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); +} diff --git a/tests/fff_print/test_data.hpp b/tests/fff_print/test_data.hpp deleted file mode 100644 index 721446bc7e..0000000000 --- a/tests/fff_print/test_data.hpp +++ /dev/null @@ -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 -#include -#include - -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 (tm); - } -}; - -/// Mesh enumeration to name mapping -extern const std::unordered_map 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 -bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; } - -template -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 &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false); -void init_print(std::initializer_list 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 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 meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list config_items, bool comments = false); -void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list config_items, bool comments = false); - -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments = false); -void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments = false); - -std::string gcode(Print& print); - -std::string slice(std::initializer_list meshes, const DynamicPrintConfig &config, bool comments = false); -std::string slice(std::initializer_list meshes, const DynamicPrintConfig &config, bool comments = false); -std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); -std::string slice(std::initializer_list meshes, std::initializer_list config_items, bool comments = false); - -// Distinct layer Z heights that carry an extrusion tagged with the given role -// comment (requires gcode_comments), e.g. "skirt", "brim", "support". -std::set layers_with_role(const std::string &gcode, const std::string &role); - -// Highest Z reached by any move in the gcode. -double max_z(const std::string &gcode); - -} } // namespace Slic3r::Test - - -#endif // SLIC3R_TEST_DATA_HPP diff --git a/tests/fff_print/test_extrusion_entity.cpp b/tests/fff_print/test_extrusion_entity.cpp index f568825baf..3287e73f01 100644 --- a/tests/fff_print/test_extrusion_entity.cpp +++ b/tests/fff_print/test_extrusion_entity.cpp @@ -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 diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 50a3c6cc58..cc3757a02d 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -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 filler(Slic3r::Fill::new_from_type("rectilinear")); filler->angle = float(-(PI)/2.0); FillParams fill_params; diff --git a/tests/fff_print/test_flow.cpp b/tests/fff_print/test_flow.cpp index 4419d7f5bf..6025bb9b56 100644 --- a/tests/fff_print/test_flow.cpp +++ b/tests/fff_print/test_flow.cpp @@ -3,7 +3,7 @@ #include #include -#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; diff --git a/tests/fff_print/test_gcode.cpp b/tests/fff_print/test_gcode.cpp deleted file mode 100644 index 2ae6a5b20b..0000000000 --- a/tests/fff_print/test_gcode.cpp +++ /dev/null @@ -1,22 +0,0 @@ -#include - -#include - -#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)); - } - } -} diff --git a/tests/fff_print/test_gcodewriter.cpp b/tests/fff_print/test_gcodewriter.cpp index 2d53dd099d..adb104a25b 100644 --- a/tests/fff_print/test_gcodewriter.cpp +++ b/tests/fff_print/test_gcodewriter.cpp @@ -3,8 +3,15 @@ #include #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(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::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>("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(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::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>("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); + } + } +} diff --git a/tests/fff_print/test_data.cpp b/tests/fff_print/test_helpers.cpp similarity index 94% rename from tests/fff_print/test_data.cpp rename to tests/fff_print/test_helpers.cpp index b1096307c8..c2e98b7777 100644 --- a/tests/fff_print/test_data.cpp +++ b/tests/fff_print/test_helpers.cpp @@ -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 mesh_names { std::pair(TestMesh::A, "A"), std::pair(TestMesh::L, "L"), std::pair(TestMesh::V, "V"), std::pair(TestMesh::_40x10, "40x10"), - std::pair(TestMesh::cube_20x20x20, "cube_20x20x20"), std::pair(TestMesh::sphere_50mm, "sphere_50mm"), std::pair(TestMesh::bridge, "bridge"), std::pair(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 &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments) +DynamicPrintConfig multifilament_config(unsigned int filaments, std::initializer_list 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(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 &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, + const std::vector> *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 &&meshes, Slic3r::Print &print, Slic3r print.set_status_silent(); } -void init_print(std::initializer_list test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in, bool comments) +void init_print(std::initializer_list test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in) { std::vector 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 input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments) +void init_print(std::initializer_list input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in) { std::vector 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 meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list 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 meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items, bool comments) +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list 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 meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments) +void init_and_process_print(std::initializer_list 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 meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments) +void init_and_process_print(std::initializer_list 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 meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments) +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list 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 meshes, Slic3r::Print &print, std::initializer_list config_items, bool comments) +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list 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 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 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 meshes, std::initializer_list config_items) +{ + Slic3r::Print print; + Slic3r::Model model; + init_print(meshes, print, model, config_items); + return gcode(print); +} + +std::string slice(std::initializer_list meshes, std::initializer_list 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 meshes, const DynamicPrintConfig &config, + const std::vector> &per_object_overrides) +{ + Slic3r::Print print; + Slic3r::Model model; + init_print(std::vector(meshes), print, model, config, &per_object_overrides); + return gcode(print); +} + +std::string slice_two_cubes_arranged(std::initializer_list config_items) +{ + return slice({ cube(20), cube(20) }, config_items); +} + +void place_two_cubes_apart(double gap, std::initializer_list 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 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 config_items) +{ + Print print; + Model model; + place_two_cubes_apart(gap, config_items, print, model); + return gcode(print); +} + std::set layers_with_role(const std::string &gcode, const std::string &role) { std::set 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 meshes, const DynamicPrintConfig &config, bool comments) +std::vector role_sequence(const std::string &gcode, const std::vector &roles) { - Slic3r::Print print; - Slic3r::Model model; - init_print(meshes, print, model, config, comments); - return gcode(print); -} - -std::string slice(std::initializer_list 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 meshes, std::initializer_list 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 meshes, std::initializer_list config_items, bool comments) -{ - Slic3r::Print print; - Slic3r::Model model; - init_print(meshes, print, model, config_items, comments); - return gcode(print); + std::vector 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 -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); } diff --git a/tests/fff_print/test_helpers.hpp b/tests/fff_print/test_helpers.hpp new file mode 100644 index 0000000000..be7d00a522 --- /dev/null +++ b/tests/fff_print/test_helpers.hpp @@ -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 +#include +#include +#include + +namespace Slic3r { namespace Test { + +constexpr double MM_PER_MIN = 60.0; + +// True when `a` and `b` are within EPSILON. +template +bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; } + +// True when `a` and `b` are within `epsilon`. +template +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(tm); + } +}; + +// TestMesh value to name mapping. +extern const std::unordered_map 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 extra = {}); + +// Apply `meshes` and config to `print`/`model`; optional per-object overrides, auto-arranged unless `arrange` is false. +void init_print(std::vector &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, + const std::vector> *per_object_overrides = nullptr, bool arrange = true); +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config()); +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config()); +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items); +void init_print(std::initializer_list meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list config_items); + +// init_print followed by process(), leaving a sliced `print` to inspect. +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config); +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, const DynamicPrintConfig& config); +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list config_items); +void init_and_process_print(std::initializer_list meshes, Slic3r::Print &print, std::initializer_list 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 meshes, const DynamicPrintConfig &config); +std::string slice(std::initializer_list meshes, const DynamicPrintConfig &config); +std::string slice(std::initializer_list meshes, std::initializer_list config_items); +std::string slice(std::initializer_list meshes, std::initializer_list 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 meshes, const DynamicPrintConfig &config, + const std::vector> &per_object_overrides); + +// Slice two auto-arranged 20mm cubes (the arranger positions them). +std::string slice_two_cubes_arranged(std::initializer_list 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 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 config_items); + +// Distinct layer Z heights carrying an extrusion of the given `role` (e.g. "skirt"). +std::set 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 role_sequence(const std::string &gcode, const std::vector &roles); + +} } // namespace Slic3r::Test + +#endif // SLIC3R_TEST_HELPERS_HPP diff --git a/tests/fff_print/test_model.cpp b/tests/fff_print/test_model.cpp index fc25e0eef8..27bf539373 100644 --- a/tests/fff_print/test_model.cpp +++ b/tests/fff_print/test_model.cpp @@ -6,7 +6,7 @@ #include -#include "test_data.hpp" +#include "test_helpers.hpp" #include "test_utils.hpp" using namespace Slic3r; diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp new file mode 100644 index 0000000000..12c1cae766 --- /dev/null +++ b/tests/fff_print/test_multifilament.cpp @@ -0,0 +1,87 @@ +#include + +#include "libslic3r/GCodeReader.hpp" + +#include "test_helpers.hpp" + +#include +#include +#include + +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 tools_for_role(const std::string& gcode, const std::string& role) +{ + std::set 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({ {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 wall_tool{ wall_filament - 1 }; + const std::set 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{ 2 }); // filament 3 + CHECK(tools_for_role(gcode, "infill") == std::set{ 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{ 0, 1 }); + CHECK(tools_for_role(gcode, "infill") == std::set{ 0 }); // infill not overridden: stays on F1 +} diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 0791d4f7f1..8ba10a6ec9 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -1,35 +1,32 @@ +#ifdef WIN32 +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + #include #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 +#include +#include 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(in)), std::istreambuf_iterator()); + + 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(self.z())); + }); + + REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3)); +} diff --git a/tests/fff_print/test_printgcode.cpp b/tests/fff_print/test_printgcode.cpp deleted file mode 100644 index 8a65679104..0000000000 --- a/tests/fff_print/test_printgcode.cpp +++ /dev/null @@ -1,629 +0,0 @@ -#ifdef WIN32 -#ifndef WIN32_LEAN_AND_MEAN -#define WIN32_LEAN_AND_MEAN -#endif -#ifndef NOMINMAX -#define NOMINMAX -#endif - #include -#endif - -#include - -#include "libslic3r/libslic3r.h" -#include "libslic3r/GCodeReader.hpp" - -#include "test_data.hpp" -#include "test_utils.hpp" - -#include -#include -#include -#include -#include -#include -#include - -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(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(in)), std::istreambuf_iterator()); - - REQUIRE_FALSE(gcode.empty()); -} - -TEST_CASE("Initial layer height is honored", "[PrintGCode]") -{ - const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, { - { "initial_layer_print_height", 0.3 }, - { "layer_height", 0.2 }, - { "z_hop", 0 } // keep recorded Z equal to the printed layer height - }); - - std::set layer_zs; - GCodeReader reader; - reader.parse_buffer(gcode, [&layer_zs] (GCodeReader& self, const GCodeReader::GCodeLine& line) { - if (line.extruding(self) && line.dist_XY(self) > 0) - layer_zs.insert(self.z()); - }); - - REQUIRE(layer_zs.size() > 1); - REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4)); - REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4)); -} - -TEST_CASE("Sequential printing follows model order", "[PrintGCode]") -{ - // Two objects of different heights, taller one added first. Orca prints - // sequential objects in model order, so the taller one is printed first. - const std::string gcode = Slic3r::Test::slice({ Slic3r::make_cube(20, 20, 20), Slic3r::make_cube(20, 20, 10) }, { - { "print_sequence", "by object" }, - { "layer_height", 0.2 }, - { "initial_layer_print_height", 0.2 }, - { "z_hop", 0 } - }); - - // The first object's height is the peak Z reached before Z drops back to the - // first layer (the object change). With by-object printing only an object - // change returns Z to the bottom. - double first_object_peak_z = 0.0; - double running_peak = 0.0; - GCodeReader reader; - reader.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) { - if (first_object_peak_z != 0.0 || !line.extruding(self)) return; // ignore travels (e.g. start-gcode Z lift) - if (running_peak > 1.0 && self.z() < 1.0) - first_object_peak_z = running_peak; - else - running_peak = std::max(running_peak, static_cast(self.z())); - }); - - REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3)); -} - -// 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(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::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>("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(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::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>("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); - } - } -} diff --git a/tests/fff_print/test_printobject.cpp b/tests/fff_print/test_printobject.cpp index c569384acb..fb7fe2c1fd 100644 --- a/tests/fff_print/test_printobject.cpp +++ b/tests/fff_print/test_printobject.cpp @@ -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 +#include 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 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)); +} diff --git a/tests/fff_print/test_skirt_brim.cpp b/tests/fff_print/test_skirt_brim.cpp index 5529abe615..a9df0fee69 100644 --- a/tests/fff_print/test_skirt_brim.cpp +++ b/tests/fff_print/test_skirt_brim.cpp @@ -9,190 +9,276 @@ #include -#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({ { 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({ + { 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 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({ { 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); }); diff --git a/tests/fff_print/test_support_material.cpp b/tests/fff_print/test_support_material.cpp index d1fc9d66e4..24c0e9cdc5 100644 --- a/tests/fff_print/test_support_material.cpp +++ b/tests/fff_print/test_support_material.cpp @@ -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); diff --git a/tests/fff_print/test_trianglemesh.cpp b/tests/fff_print/test_trianglemesh.cpp index e201eed3a7..b21f6299fd 100644 --- a/tests/fff_print/test_trianglemesh.cpp +++ b/tests/fff_print/test_trianglemesh.cpp @@ -12,14 +12,14 @@ #include //#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 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 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");