Merge branch 'main' into feat/plugin-feature

Resolve five conflicts, all of which needed both sides rather than a pick:

- BackgroundSlicingProcess: ours was a pure tabs->spaces reformat of base, so
  keep main's per-filament volume/nozzle map read-back (its only change here).
- GUI_App: main's #12506 else-if attached to an `if` this branch deleted;
  re-expressed onto the same-agent early-return path (the agent factory caches
  per id, so pointer equality is the same predicate).
- MainFrame: both sides relocated Sync Presets independently; keep main's
  push_notification plus the branch's Plugins menu items.
- Tab: the "TODO: Orca: Support hybrid" blocks were unchanged base, not a branch
  decision; take main's enabled Hybrid to match the already auto-merged siblings.
- test_config: union of both sides' cases (6 plugin + 9 multi-nozzle).
This commit is contained in:
SoftFever
2026-07-15 17:18:46 +08:00
986 changed files with 210610 additions and 18572 deletions

View File

@@ -1,16 +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_slicing_pipeline_hook.cpp

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

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

View File

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

View File

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

View File

@@ -7,7 +7,7 @@
#include "libslic3r/Point.hpp"
#include "libslic3r/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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,420 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "test_utils.hpp"
#include <fstream>
#include <map>
#include <memory>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
// Regression coverage for filament/tool-change time being folded into the first
// pending motion block (an extrusion move) instead of the tool-change move, and
// for that delay being dropped entirely when too few motion blocks precede the
// change. See BambuStudio "seperate flush time from other types" (c54a8333c7)
// and the follow-up "unprocessed addtional time" fix (27ef0b1bef).
namespace {
constexpr size_t NORMAL = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
FullPrintConfig make_config(double load_time, double unload_time, double tool_change_time)
{
FullPrintConfig config; // default-initialized with the built-in defaults
config.gcode_flavor.value = gcfMarlinFirmware;
// Two filaments, both assigned to the same (single) extruder, so a T1 after
// T0 is a same-extruder filament swap that costs unload + load time.
config.filament_diameter.values = {1.75, 1.75};
config.filament_map.values = {1, 1};
config.machine_load_filament_time.value = load_time;
config.machine_unload_filament_time.value = unload_time;
config.machine_tool_change_time.value = tool_change_time;
return config;
}
void run_processor(GCodeProcessor& proc, const FullPrintConfig& config, const char* gcode)
{
// reserved_tag() selects between two tag tables based on this shared static, and
// other tests in the binary mutate it -- pin it so our "; FEATURE:" role tags are
// parsed deterministically regardless of test execution order.
GCodeProcessor::s_IsBBLPrinter = true;
ScopedTemporaryFile temp(".gcode");
{
std::ofstream os(temp.string());
os << gcode;
}
proc.apply_config(config);
// No producer marker in the gcode, so process_file keeps our applied config.
proc.process_file(temp.string());
}
// Estimated time per extrusion role, grouped exactly the way libvgcode builds the
// feature-type legend: sum MoveVertex.time over EMoveType::Extrude moves keyed by
// extrusion_role (see ViewerImpl.cpp:1017 -- only Extrude moves are counted).
std::map<ExtrusionRole, double> role_times(const GCodeProcessorResult& r)
{
std::map<ExtrusionRole, double> m;
for (const auto& mv : r.moves)
if (mv.type == EMoveType::Extrude)
m[mv.extrusion_role] += mv.time[NORMAL];
return m;
}
// Sum of estimated time attributed to tool-change moves.
double sum_tool_change_time(const GCodeProcessorResult& r)
{
double t = 0.0;
for (const auto& mv : r.moves)
if (mv.type == EMoveType::Tool_change)
t += mv.time[NORMAL];
return t;
}
// Total filament-change delay, accumulated independently of the timing machinery.
double filament_change_delay(const GCodeProcessorResult& r)
{
const auto& s = r.print_statistics;
return s.total_filament_load_time + s.total_filament_unload_time + s.total_tool_change_time;
}
} // namespace
TEST_CASE("Filament-change time is attributed to tool-change moves, not extrusion roles", "[GCodeTiming]")
{
// Relative extrusion (M83) so every "E5" is a real 5mm extrusion move rather
// than a zero-delta travel. Two real travels precede T0 so its delay is flushed
// cleanly. The extrusions after T0 span several roles (Outer wall, Sparse infill,
// Inner wall); the first pending block at T1 is an "Outer wall" move, so the
// buggy code folds the T1 delay into that role. The per-role check below verifies
// EVERY role stays clean, not just one, and catches any role-to-role misattribution.
const char* gcode =
"M83\n"
"; FEATURE: Outer wall\n"
"G1 X10 Y10 Z0.2 F600\n"
"G1 X0 Y0 F6000\n"
"T0\n"
"; FEATURE: Outer wall\n"
"G1 X50 Y0 E5 F1800\n"
"G1 X50 Y50 E5\n"
"; FEATURE: Sparse infill\n"
"G1 X0 Y50 E5\n"
"G1 X0 Y0 E5\n"
"T1\n"
"; FEATURE: Inner wall\n"
"G1 X50 Y0 E5\n"
"G1 X50 Y50 E5\n";
GCodeProcessor proc_zero;
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
const GCodeProcessorResult& r_zero = proc_zero.get_result();
const double load = 10.0;
const double unload = 5.0;
GCodeProcessor proc_delay;
run_processor(proc_delay, make_config(load, unload, 0.0), gcode);
const GCodeProcessorResult& r_delay = proc_delay.get_result();
const double delay = filament_change_delay(r_delay);
// Preconditions: the filament changes were charged, and cost nothing in the
// zero-time baseline.
REQUIRE(delay > 0.0);
REQUIRE_THAT(filament_change_delay(r_zero), WithinAbs(0.0, 1e-9));
// The delay must not inflate the time of ANY extrusion role. Compare the full
// per-role breakdown (exactly how the feature-type legend is built) between the
// zero-delay and delayed runs -- every role must match to within tolerance.
const auto roles_zero = role_times(r_zero);
const auto roles_delay = role_times(r_delay);
// Guard: the gcode must genuinely exercise multiple distinct roles (Outer wall,
// Sparse infill, Inner wall), otherwise this check would silently cover only one.
REQUIRE(roles_zero.size() >= 3);
REQUIRE(roles_zero.size() == roles_delay.size());
for (const auto& [role, zero_time] : roles_zero) {
INFO("extrusion role index = " << static_cast<int>(role));
REQUIRE(roles_delay.count(role) == 1);
REQUIRE_THAT(roles_delay.at(role), WithinAbs(zero_time, 1e-2));
}
// The delay must instead land on the tool-change moves, so per-move consumers
// (layer-time view, layer slider) stay consistent.
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(delay, 1e-2));
// Both tool changes occur on layer 1, so the delay must also be reflected in
// the first-layer time.
const double first_layer_delta = proc_delay.get_first_layer_time(PrintEstimatedStatistics::ETimeMode::Normal)
- proc_zero.get_first_layer_time(PrintEstimatedStatistics::ETimeMode::Normal);
REQUIRE_THAT(first_layer_delta, WithinAbs(delay, 1e-2));
}
TEST_CASE("Filament-change time is not dropped when few motion blocks precede the change", "[GCodeTiming]")
{
// Only a single motion block precedes T0, so the buggy code's "fewer than two
// pending blocks" early-out discards that filament-change delay entirely,
// making the total print time inconsistent with the reported statistics.
const char* gcode =
"; FEATURE: Outer wall\n"
"G1 X10 Y10 Z0.2 F600\n"
"T0\n"
"G1 X50 Y0 E5 F1800\n"
"G1 X50 Y50 E5\n"
"T1\n"
"G1 X0 Y50 E5\n"
"G1 X0 Y0 E5\n";
GCodeProcessor proc_zero;
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
const double load = 10.0;
const double unload = 5.0;
GCodeProcessor proc_delay;
run_processor(proc_delay, make_config(load, unload, 0.0), gcode);
const GCodeProcessorResult& r_delay = proc_delay.get_result();
const double delay = filament_change_delay(r_delay);
REQUIRE(delay > 0.0);
// Every second of reported filament-change delay must be present in the total
// estimated print time; none may be silently dropped.
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
}
TEST_CASE("Back-to-back tool changes buffer then merge into one tool-change block", "[GCodeTiming]")
{
// T0 is the very first line: the block queue is empty when its delay is synchronized,
// so with only the single (artificial) tool-change block queued the delay can't be
// attributed yet and is buffered. T1 follows immediately with no motion between; its
// synchronize now sees two tool-change blocks queued, so its own delay joins the buffered
// T0 entry at application time, the two same-type entries merge into one, and the sum
// lands entirely on the first tool-change block. The trailing travels leave both runs
// with >= 2 blocks so their end-of-file flush is identical and cancels in every delta.
const char* gcode =
"T0\n" // first charged change (load only); empty queue -> buffers (Tool_change,10)
"T1\n" // same-extruder swap (unload+load); merges with buffered T0 entry to (Tool_change,25)
"G1 X10 Y0 Z0.2 F6000\n" // travels: keep >= 2 blocks queued at EOF (flushed identically by both runs)
"G1 X10 Y10\n"
"G1 X0 Y10\n";
GCodeProcessor proc_zero;
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
const GCodeProcessorResult& r_zero = proc_zero.get_result();
GCodeProcessor proc_delay;
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
const GCodeProcessorResult& r_delay = proc_delay.get_result();
// T0 load 10 + T1 unload 5 + T1 load 10 = 25.
const double delay = filament_change_delay(r_delay);
REQUIRE(delay > 0.0);
REQUIRE_THAT(delay, WithinAbs(25.0, 1e-6));
REQUIRE_THAT(filament_change_delay(r_zero), WithinAbs(0.0, 1e-9));
// The whole buffered-then-merged delay must reach the total print time.
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
// ...and must land on the tool-change moves, not on any extrusion role.
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(25.0, 1e-2));
REQUIRE_THAT(sum_tool_change_time(r_zero), WithinAbs(0.0, 1e-9));
// Characterization (documents the current merge-collapse behavior, not a correctness
// requirement): the two buffered same-type entries combine onto the FIRST artificial
// tool-change block; the second receives nothing. Had the merge regressed, the 10 and 15
// would land on separate moves instead of 25 and 0.
std::vector<double> tc;
for (const auto& mv : r_delay.moves)
if (mv.type == EMoveType::Tool_change)
tc.push_back(mv.time[NORMAL]);
REQUIRE(tc.size() >= 2);
REQUIRE_THAT(tc[0], WithinAbs(25.0, 1e-2));
REQUIRE_THAT(tc[1], WithinAbs(0.0, 1e-9));
}
TEST_CASE("Trailing tool change at end of file is drained, not dropped", "[GCodeTiming]")
{
// A tool change is the last line of the file, with only its single artificial block
// queued. Its delay is buffered (fewer than two blocks) and there is no later motion to
// flush it, so only the finalization pass can attribute it. Without the end-of-file drain
// the delay would be stranded in the buffer and the total print time would disagree with
// the reported filament-change statistics.
const char* gcode =
"G1 X10 Y0 Z0.2 F6000\n" // three travels -> three blocks queued (no E, so no filament is selected)
"G1 X10 Y10\n"
"G1 X0 Y10\n"
"G4 S0\n" // dwell with S present -> full flush; queue and buffer now empty
"T0\n"; // trailing change, nothing after: buffers (Tool_change,10), one block queued
GCodeProcessor proc_zero;
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
const GCodeProcessorResult& r_zero = proc_zero.get_result();
GCodeProcessor proc_delay;
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
const GCodeProcessorResult& r_delay = proc_delay.get_result();
// T0 is the first charged change on an empty extruder, so it costs the load time only.
const double delay = filament_change_delay(r_delay);
REQUIRE(delay > 0.0);
REQUIRE_THAT(delay, WithinAbs(10.0, 1e-6));
// The trailing change's delay must survive to the total: the zero run buffers nothing and
// drops its artificial block, so the motion cancels and the delta is exactly the drained delay.
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
// The size-1 drain runs the body, so the delay lands on the artificial tool-change move.
REQUIRE_THAT(sum_tool_change_time(r_delay), WithinAbs(10.0, 1e-2));
REQUIRE_THAT(sum_tool_change_time(r_zero), WithinAbs(0.0, 1e-9));
}
TEST_CASE("Carried-forward tool-change delay reaches the total without polluting roles", "[GCodeTiming]")
{
// A wildcard dwell delay is buffered ahead of the tool-change delay, so when the blocks
// are next flushed the dwell's (Noop) entry consumes the artificial tool-change block and
// the tool-change entry finds no matching block and carries forward. It stays unmatched
// through the remaining extrusion moves and is only resolved at finalization, where the
// end-of-file fold adds it to the machine total and the custom-gcode cache -- never to a
// move vertex, so it cannot leak into an extrusion role's time.
const char* gcode =
"M83\n"
"G4 S3\n" // empty queue -> buffers (Noop,3) [wildcard delay]
"T0\n" // one block queued -> buffers (Tool_change,10) behind the dwell
"; FEATURE: Inner wall\n"
"G1 X20 Y0 Z0.2 E5 F1800\n" // extrusion m1: queue is [artificial_TC0, m1]
"G4 S0\n" // flush: (Noop,3) consumes artificial_TC0; (Tool_change,10) carries forward
"G1 X20 Y20 E5\n" // extrusion m2
"G1 X0 Y20 E5\n"; // extrusion m3: at EOF queue is [m2, m3], buffer is [(Tool_change,10)]
GCodeProcessor proc_zero;
run_processor(proc_zero, make_config(0.0, 0.0, 0.0), gcode);
const GCodeProcessorResult& r_zero = proc_zero.get_result();
GCodeProcessor proc_delay;
run_processor(proc_delay, make_config(10.0, 5.0, 0.0), gcode);
const GCodeProcessorResult& r_delay = proc_delay.get_result();
// T0 is the first charged change (load only); the fixed dwell delays are not in these counters.
const double delay = filament_change_delay(r_delay);
REQUIRE(delay > 0.0);
REQUIRE_THAT(delay, WithinAbs(10.0, 1e-6));
// The stranded tool-change delay must be drained into the total, not dropped. The 3s dwell
// is identical in both runs and cancels along with all motion, leaving exactly the delay.
const double total_delta = proc_delay.get_time(PrintEstimatedStatistics::ETimeMode::Normal)
- proc_zero.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
REQUIRE_THAT(total_delta, WithinAbs(delay, 1e-2));
// Pollution safety: the drained delay must NOT appear in any extrusion role. Every role's
// time must match between the zero and delayed runs -- this is what the total-only fold buys.
const auto rz = role_times(r_zero);
const auto rd = role_times(r_delay);
REQUIRE(rz.size() >= 1);
REQUIRE(rz.size() == rd.size());
for (const auto& [role, zero_time] : rz) {
INFO("extrusion role index = " << static_cast<int>(role));
REQUIRE(rd.count(role) == 1);
REQUIRE_THAT(rd.at(role), WithinAbs(zero_time, 1e-2));
}
}
TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][MultiNozzle]")
{
// Single physical extruder carrying two nozzle variants: machine slot 0 (Standard) caps X/Y
// speed at 200 mm/s, slot 1 (High Flow) at 50 mm/s. The estimator must clamp each move by the
// slot of the nozzle the active filament occupies -- resolved from the grouping context handed
// over before the replay plus the occupancy recorder, i.e. the exact in-slicer streaming path.
FullPrintConfig config = make_config(0.0, 0.0, 0.0);
config.extruder_type.values = {static_cast<int>(etDirectDrive)};
config.printer_extruder_id.values = {1, 1};
config.printer_extruder_variant.values = {"Direct Drive Standard", "Direct Drive High Flow"};
// Slot-major layout: [slot0-Normal, slot0-Stealth, slot1-Normal, slot1-Stealth].
config.machine_max_speed_x.values = {200., 200., 50., 50.};
config.machine_max_speed_y.values = {200., 200., 50., 50.};
config.machine_max_speed_z.values = {200., 200., 50., 50.};
config.machine_max_speed_e.values = {200., 200., 50., 50.};
// Keep acceleration and jerk far from limiting so move times are speed-dominated.
for (auto *accel : {&config.machine_max_acceleration_x, &config.machine_max_acceleration_y,
&config.machine_max_acceleration_z, &config.machine_max_acceleration_e})
accel->values = {100000., 100000., 100000., 100000.};
config.machine_max_acceleration_travel.values = {100000., 100000.};
config.machine_max_acceleration_extruding.values = {100000., 100000.};
config.machine_max_jerk_x.values = {10000., 10000.};
config.machine_max_jerk_y.values = {10000., 10000.};
config.machine_max_jerk_z.values = {10000., 10000.};
config.machine_max_jerk_e.values = {10000., 10000.};
// Grouping stub: filament 0 lives on the Standard nozzle (slot 0), filament 1 on the
// High Flow nozzle (slot 1), both mounted on extruder 0.
std::vector<MultiNozzleUtils::NozzleInfo> nozzles;
{
MultiNozzleUtils::NozzleInfo n;
n.diameter = "0.4";
n.volume_type = nvtStandard; n.extruder_id = 0; n.group_id = 0; nozzles.push_back(n);
n.volume_type = nvtHighFlow; n.extruder_id = 0; n.group_id = 1; nozzles.push_back(n);
}
std::vector<int> filament_nozzle_map = {0, 1};
std::vector<unsigned int> used_filaments = {0, 1};
auto group = MultiNozzleUtils::LayeredNozzleGroupResult::create(filament_nozzle_map, nozzles, used_filaments);
REQUIRE(group.has_value());
auto context = std::make_shared<MultiNozzleUtils::LayeredNozzleGroupResult>(*group);
// Two identical 100 mm X travels, one per filament; T..H.. carries the target nozzle id.
// The trailing 1 mm move keeps two blocks queued at finalize, so the measured move's time is
// flushed (a lone final block is never attributed); it adds 1 mm to the second bucket.
const char* gcode =
"M83\n"
"T0 H0\n"
"G1 X100 F30000\n"
"T1 H1\n"
"G1 X0 F30000\n"
"G1 X1 F30000\n";
// Travel time accumulated after each tool-change move (bucket 0 = before any T).
auto travel_times_by_tool = [](const GCodeProcessorResult& r) {
std::vector<double> out(1, 0.0);
for (const auto& mv : r.moves) {
if (mv.type == EMoveType::Tool_change)
out.push_back(0.0);
else if (mv.type == EMoveType::Travel)
out.back() += mv.time[NORMAL];
}
return out;
};
SECTION("the move on the High Flow nozzle is clamped by its own slot") {
GCodeProcessor proc;
proc.initialize_from_context(context);
run_processor(proc, config, gcode);
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 200.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 50.0, 0.10));
}
SECTION("an emitted envelope line reaches every slot") {
const std::string enveloped = std::string("M201 X20000\nM203 X80\n") + gcode;
GCodeProcessor proc;
proc.initialize_from_context(context);
run_processor(proc, config, enveloped.c_str());
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 80.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 80.0, 0.10));
}
SECTION("no grouping context degrades to slot 0") {
GCodeProcessor proc;
run_processor(proc, config, gcode);
auto times = travel_times_by_tool(proc.get_result());
REQUIRE(times.size() == 3);
REQUIRE_THAT(times[1], Catch::Matchers::WithinRel(100.0 / 200.0, 0.10));
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10));
}
}

View File

@@ -1,10 +1,33 @@
#include <catch2/catch_all.hpp>
#include <cstdlib>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/ModelArrange.hpp"
#include <boost/filesystem.hpp>
#include "test_helpers.hpp"
using namespace Slic3r;
using namespace Slic3r::Test;
// Arrange on a finite bed, not an unbounded InfiniteBed: the latter places items
// near INT64_MIN/4 (~2.3e18), which reaches ClipperLib's coordinate limit and throws
// "Coordinate outside allowed range" on Windows/arm64. A 500x500 bed keeps coordinates
// small while still covering large printers.
static void arrange_objects_on_test_bed(Model &model, const DynamicPrintConfig &config)
{
const BoundingBox bed{Point::new_scale(0., 0.), Point::new_scale(500., 500.)};
arrange_objects(model, bed, ArrangeParams{scaled(min_object_distance(config))});
}
SCENARIO("set_speed emits values with fixed-point output.", "[GCodeWriter]") {
@@ -57,3 +80,743 @@ SCENARIO("z_hop lifts the nozzle when a lift is requested", "[GCodeWriter]") {
}
}
}
SCENARIO("Origin manipulation", "[GCodeWriter]") {
Slic3r::GCode gcodegen;
WHEN("set_origin to (10,0)") {
gcodegen.set_origin(Vec2d(10,0));
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
}
WHEN("set_origin to (10,0) and translate by (5, 5)") {
gcodegen.set_origin(Vec2d(10,0));
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
THEN("origin returns reference to point") {
REQUIRE(gcodegen.origin() == Vec2d(15,5));
}
}
}
// Verify that emit_machine_limits_to_gcode emits the correct max value across
// used extruders (regression for commit b4ee665: "Emit max value of machine
// limit among used extruders").
TEST_CASE("Machine envelope emits max limit among used extruders", "[GCodeWriter]")
{
SECTION("Single extruder emits its configured values") {
const std::string gcode = Slic3r::Test::slice({ cube(20) }, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin2" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// stride-2 options: (normal, silent)
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "700,800" },
{ "machine_max_acceleration_z", "100,200" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
// stride-2 options: (normal, silent)
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("M201 uses the normal acceleration values") {
REQUIRE(gcode.find("M201 X500 Y700 Z100 E5000") != std::string::npos);
}
THEN("M203 uses the speed values") {
REQUIRE(gcode.find("M203 X100 Y110 Z10 E50") != std::string::npos);
}
THEN("M204 (Marlin 2) uses extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P1200 R1400 T1600") != std::string::npos);
}
THEN("M205 uses the jerk values") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
THEN("M205 J uses the junction deviation") {
REQUIRE(gcode.find("M205 J0.020") != std::string::npos);
}
}
SECTION("Legacy Marlin flavor emits correct format") {
const std::string gcode = Slic3r::Test::slice({ cube(20) }, {
{ "emit_machine_limits_to_gcode", "1" },
{ "gcode_flavor", "marlin" },
{ "gcode_comments", "1" },
{ "machine_start_gcode", "" },
{ "layer_height", "0.2" },
{ "initial_layer_print_height", "0.2" },
{ "initial_layer_line_width", "0" },
{ "z_hop", "0" },
// All machine limits must be provided — defaults are empty vectors.
{ "machine_max_acceleration_x", "500,600" },
{ "machine_max_acceleration_y", "500,600" },
{ "machine_max_acceleration_z", "500,600" },
{ "machine_max_acceleration_e", "5000,6000" },
{ "machine_max_acceleration_extruding", "1200,1300" },
{ "machine_max_acceleration_retracting", "1400,1500" },
{ "machine_max_acceleration_travel", "1600,1700" },
{ "machine_max_speed_x", "100,100" },
{ "machine_max_speed_y", "110,110" },
{ "machine_max_speed_z", "10,10" },
{ "machine_max_speed_e", "50,50" },
{ "machine_max_jerk_x", "8,8" },
{ "machine_max_jerk_y", "9,9" },
{ "machine_max_jerk_z", "0.4,0.4" },
{ "machine_max_jerk_e", "5,5" },
{ "machine_max_junction_deviation", "0.02,0.03" },
});
THEN("Legacy Marlin: M204 travel_acc = extruding_acc") {
// gcfMarlinLegacy uses extruding acc for travel too
REQUIRE(gcode.find("M204 P1200 R1400 T1200") != std::string::npos);
}
THEN("Legacy Marlin: M205 uses mm/sec format") {
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
}
}
SECTION("Multi extruder - max of used extruders is emitted") {
// Build config with 2 extruders that have *different* machine limits.
// Extruder 1 has higher values; the emitted G-code must use the max.
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
// Print basics
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
// Print objects sequentially so each uses its own extruder without
// wipe-tower / tool-change complexity.
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
// filament_map maps filament slot index (1-based) → logical extruder ID (1-based).
// Default [1] maps everything to extruder 0. Need [1, 2] for two distinct extruders.
// fmmManual prevents auto-computation from overwriting the explicit mapping.
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
// flush_volumes_matrix must be filament_count^2 * heads_count entries.
// 2 filaments * 2 * 1 head = 4 entries (all zero — flush volumes not tested here).
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// Machine limits: extruder 0 low, extruder 1 high
// Stride-2 (normal, silent pairs): e0_n, e0_s, e1_n, e1_s
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({700, 0, 1100, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 300, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 8000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({1200, 0, 2200, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({1400, 0, 2400, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({1600, 0, 2600, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({8, 0, 12, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({9, 0, 13, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.6, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({5, 0, 10, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.05, 0}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
// obj1 uses default extruder=1 (0-based index 0)
auto* obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects_on_test_bed(model, config);
for (auto* mo : model.objects) {
mo->ensure_on_bed();
print.auto_assign_extruders(mo);
}
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
std::string gcode = Slic3r::Test::gcode(print);
THEN("M201 contains max (extruder 1's) acceleration values") {
REQUIRE(gcode.find("M201 X1000 Y1100 Z300 E8000") != std::string::npos);
}
THEN("M203 contains max speed values") {
REQUIRE(gcode.find("M203 X200 Y210 Z30 E80") != std::string::npos);
}
THEN("M204 contains max extruding / retracting / travel") {
REQUIRE(gcode.find("M204 P2200 R2400 T2600") != std::string::npos);
}
THEN("M205 contains max jerk values") {
REQUIRE(gcode.find("M205 X12.00 Y13.00 Z0.60 E10.00") != std::string::npos);
}
THEN("M205 contains max m_max_junction_deviation ") {
REQUIRE(gcode.find("M205 J0.050") != std::string::npos);
}
}
}
// Verify that the EXTRUDER_LIMIT macro (GCodeWriter.cpp) correctly:
// 1) Uses the active extruder's specific limit when filament() is known.
// 2) Falls back to the maximum of all extruder limits when filament() is nullptr.
//
// These two behaviours were introduced in:
// - "Use per-extruder motion limit" (1ab34a7454)
// - "Use max limit when current extruder is unknown" (b7240ab1c6)
TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[GCodeWriter]")
{
// --- Build config with 2 extruders that have different machine limits ---
// Extruder 0: low limits
// Extruder 1: high limits
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
// 2 extruders, 2 filaments
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
// --- Machine limits (stride-2: e0_n, e0_s, e1_n, e1_s) ---
// Extruder 0 has LOW limits, Extruder 1 has HIGH limits.
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({500, 0, 1000, 0}));
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 5000, 0}));
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({500, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({600, 0, 2000, 0}));
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({700, 0, 2500, 0}));
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({5, 0, 15, 0}));
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({6, 0, 16, 0}));
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.8, 0}));
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({3, 0, 8, 0}));
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.08, 0}));
// --- Print acceleration: 1500 mm/s² ---
// Exceeds extruder 0's limit (500) → should be clamped to 500.
// Does NOT exceed extruder 1's limit (2000) → passes through as 1500.
config.set_key_value("default_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("outer_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("inner_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("top_surface_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("initial_layer_acceleration", new ConfigOptionFloats({1500, 1500}));
config.set_key_value("travel_acceleration", new ConfigOptionFloats({1500, 1500}));
// Model: two objects assigned to different extruders
Model model;
auto* obj1 = model.add_object();
obj1->add_volume(cube(20));
obj1->add_instance();
auto* obj2 = model.add_object();
obj2->add_volume(cube(20));
obj2->add_instance();
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
Print print;
arrange_objects_on_test_bed(model, 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);
}
}
}
SCENARIO("Extruder reads the injected config column", "[GCodeWriter][H2C]") {
GIVEN("A writer whose per-variant arrays hold three columns for two filaments") {
GCodeWriter writer;
// Column layout after a migrating regroup: filament 0 -> column 0, filament 1 ->
// columns 1 (its first variant) and 2 (its second variant).
writer.config.retraction_length.values = {0.8, 0.5, 1.2};
writer.config.z_hop.values = {0.4, 0.6, 0.9};
writer.config.retraction_speed.values = {30., 40., 50.};
writer.config.filament_flow_ratio.values = {0.98, 1.0, 1.02};
// Filament-indexed arrays keep one entry per filament.
writer.config.filament_diameter.values = {1.75, 1.75};
writer.set_extruders({0, 1});
writer.toolchange(1, 1);
Extruder *fil = writer.filament();
REQUIRE(fil != nullptr);
REQUIRE(fil->id() == 1);
const double crossection = 1.75 * 1.75 * 0.25 * PI;
WHEN("no column has been injected") {
THEN("the getters read the filament id's column") {
REQUIRE(fil->config_index() == 1);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(fil->retract_lift(), Catch::Matchers::WithinAbs(0.6, 1e-9));
REQUIRE(fil->retract_speed() == 40);
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.0 / crossection, 1e-9));
}
}
WHEN("the second variant column is injected") {
fil->set_config_index(2);
THEN("the getters follow the column and the flow cache is rescaled") {
REQUIRE(fil->config_index() == 2);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(1.2, 1e-9));
REQUIRE_THAT(fil->retract_lift(), Catch::Matchers::WithinAbs(0.9, 1e-9));
REQUIRE(fil->retract_speed() == 50);
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.02 / crossection, 1e-9));
}
THEN("filament-indexed reads keep using the filament id") {
REQUIRE_THAT(fil->filament_diameter(), Catch::Matchers::WithinAbs(1.75, 1e-9));
}
}
WHEN("a negative index is injected") {
fil->set_config_index(2);
fil->set_config_index(-1);
THEN("resolution resets to the filament id") {
REQUIRE(fil->config_index() == 1);
REQUIRE_THAT(fil->retraction_length(), Catch::Matchers::WithinAbs(0.5, 1e-9));
REQUIRE_THAT(fil->e_per_mm3(), Catch::Matchers::WithinRel(1.0 / crossection, 1e-9));
}
}
}
}
// Numeric argument of every line starting with `prefix`, in file order.
static std::vector<int> collect_line_args(const std::string &gcode, const std::string &prefix)
{
std::vector<int> values;
std::istringstream stream(gcode);
std::string line;
while (std::getline(stream, line))
if (line.compare(0, prefix.size(), prefix) == 0)
values.push_back(std::atoi(line.c_str() + int(prefix.size())));
return values;
}
static int count_lines_with_prefix(const std::string &gcode, const std::string &prefix)
{
return (int) collect_line_args(gcode, prefix).size();
}
// A toolchange ordinal sequence is healthy when it advances by exactly one per
// change block; a change-less prime-tower visit must not consume an ordinal.
static bool ordinals_consecutive(const std::vector<int> &values)
{
for (size_t i = 1; i < values.size(); ++i)
if (values[i] != values[i - 1] + 1)
return false;
return true;
}
SCENARIO("Toolchange emission and prefix per printer kind", "[GCodeWriter][H2C]") {
GIVEN("A dual-extruder writer with two filaments") {
GCodeWriter writer;
writer.config.filament_diameter.values = {1.75, 1.75};
writer.set_extruders({0, 1});
WHEN("the printer is a BBL machine") {
writer.set_is_bbl_machine(true);
THEN("the toolchange prefix is the plain T command") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::Equals("T"));
}
THEN("toolchange emits a single M1020 with the nozzle id") {
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("M1020 S1 H0"));
REQUIRE_THAT(gcode, !Catch::Matchers::StartsWith("T1"));
}
THEN("the other filament and nozzle emit their own ids") {
REQUIRE_THAT(writer.toolchange(0, 1), Catch::Matchers::ContainsSubstring("M1020 S0 H1"));
}
THEN("an unresolved nozzle keeps the literal -1 convention") {
REQUIRE_THAT(writer.toolchange(1, -1), Catch::Matchers::ContainsSubstring("M1020 S1 H-1"));
}
}
WHEN("the printer is a BBL machine with manual filament change") {
writer.set_is_bbl_machine(true);
writer.config.manual_filament_change.value = true;
THEN("the manual tag wins over the M1020 form") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::StartsWith(";"));
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring(writer.toolchange_prefix() + "1"));
REQUIRE_THAT(gcode, !Catch::Matchers::ContainsSubstring("M1020"));
}
}
WHEN("the printer is not a BBL machine") {
THEN("toolchange keeps the plain T command") {
REQUIRE_THAT(writer.toolchange_prefix(), Catch::Matchers::Equals("T"));
const std::string gcode = writer.toolchange(1, 0);
REQUIRE_THAT(gcode, Catch::Matchers::StartsWith("T1"));
REQUIRE_THAT(gcode, !Catch::Matchers::ContainsSubstring("M1020"));
}
}
}
}
// Shared dual-extruder printer config for the toolchange-count scenarios below.
static DynamicPrintConfig dual_extruder_toolchange_config()
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(false));
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., 0.}));
// The change block carries both a real toolchange command and the ordinal
// placeholder the stock profiles feed to the firmware.
config.set_key_value("change_filament_gcode",
new ConfigOptionString("T[next_filament_id]\nM620 O{toolchange_count + 1}\n"));
// 2 extruders, one filament each (manual map so nothing regroups them).
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("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
config.set_key_value("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, 140, 140, 0}));
return config;
}
SCENARIO("Change blocks carry consecutive toolchange ordinals without a duplicate command", "[GCodeWriter][H2C]") {
GIVEN("Two sequentially printed objects on different extruders of a BBL machine") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
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));
auto slice_to_gcode = [&]() {
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, 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();
return Slic3r::Test::gcode(print);
};
WHEN("the change block already changes the tool") {
const std::string gcode = slice_to_gcode();
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("each change block advances the ordinal by exactly one, without inflation") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
THEN("the writer's own command is suppressed as a duplicate") {
REQUIRE(count_lines_with_prefix(gcode, "M1020") == 0);
REQUIRE(count_lines_with_prefix(gcode, "T1") >= 1);
}
}
WHEN("the change block does not change the tool itself") {
config.set_key_value("change_filament_gcode",
new ConfigOptionString("M620 O{toolchange_count + 1}\n"));
const std::string gcode = slice_to_gcode();
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("the writer's toolchange survives and carries a nozzle id") {
REQUIRE(count_lines_with_prefix(gcode, "M1020 S1 H") >= 1);
}
THEN("the ordinal sequence stays consecutive") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
}
}
}
SCENARIO("Prime-tower visits without a filament change do not advance the toolchange ordinal", "[GCodeWriter][H2C]") {
GIVEN("A print whose only filament change happens far above the bed") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("enable_prime_tower", new ConfigOptionBool(true));
// Filament 2 is used only above z=6, so every tower layer below it is a
// change-less visit — the exact geometry that used to inflate the ordinal.
Model model;
auto *obj = model.add_object();
obj->add_volume(cube(10));
obj->add_instance();
DynamicPrintConfig range_config;
range_config.set_key_value("extruder", new ConfigOptionInt(2));
// Every layer range must carry a layer_height (see layer_height_profile_from_ranges).
range_config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
obj->layer_config_ranges[{6.0, 10.0}].assign_config(std::move(range_config));
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, 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();
const std::string gcode = Slic3r::Test::gcode(print);
WHEN("the print is exported") {
const std::vector<int> ordinals = collect_line_args(gcode, "M620 O");
THEN("the prime-tower toolchange path was exercised") {
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("CP TOOLCHANGE START"));
}
THEN("dozens of change-less tower layers consume no ordinal") {
REQUIRE(!ordinals.empty());
REQUIRE(ordinals_consecutive(ordinals));
REQUIRE(ordinals.front() <= 3);
}
THEN("no duplicate toolchange command follows the change block") {
REQUIRE(count_lines_with_prefix(gcode, "M1020") == 0);
}
}
}
}
// ---------------------------------------------------------------------------
// Real-profile toolchange coverage, targeted. The all-vendors sweep in test_profile_slicing.cpp now
// slices a two-colour cube per printer, so it already expands every shipped change_filament_gcode with
// each printer's DEFAULT extruder variants (both the single-nozzle append_tcr and dual-nozzle set_extruder
// paths). What that sweep can't reach is a variant-conditional branch the defaults never select — H2D's
// change gcode has an `== "Direct Drive TPU High Flow"` block. This scenario forces that branch by handing
// the extruders distinct kits, so an unregistered placeholder inside it still throws "Variable does not
// exist" here instead of only in the field.
// ---------------------------------------------------------------------------
// Two 20mm cubes on separate extruders of a BBL machine, printed by object so exactly
// one real toolchange fires and drives the change_filament_gcode. Returns the g-code.
static std::string slice_two_object_bbl(DynamicPrintConfig &config)
{
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
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));
Print print;
print.is_BBL_printer() = true;
arrange_objects_on_test_bed(model, 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();
return Slic3r::Test::gcode(print);
}
// The real change_filament_gcode of a shipped "<printer> 0.4 nozzle" machine profile.
static std::string shipped_change_filament_gcode(const std::string &printer)
{
const std::string path = std::string(PROFILES_DIR) + "/BBL/machine/Bambu Lab " + printer + " 0.4 nozzle.json";
// PROFILES_DIR is an absolute path baked in at build time; a sparse test checkout
// without resources/ leaves it missing. Skip rather than dereference a config that
// never loaded - this is the only fff_print test that reads a shipped profile.
if (!boost::filesystem::exists(path))
SKIP("shipped profile not present in this checkout: " << path);
DynamicPrintConfig config;
std::map<std::string, std::string> key_values;
std::string reason;
config.load_from_json(path, ForwardCompatibilitySubstitutionRule::Enable, key_values, reason);
// Fail loudly on a malformed/renamed profile instead of null-dereferencing in opt_string.
INFO("profile: " << path << (reason.empty() ? "" : (" load reason: " + reason)));
REQUIRE(config.has("change_filament_gcode"));
return config.opt_string("change_filament_gcode");
}
SCENARIO("Toolchange gcode resolves old/new_extruder_variant from printer_extruder_variant", "[GCodeWriter][H2C]")
{
GIVEN("a BBL dual-extruder print whose change gcode reads the extruder-variant placeholders") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
// A distinctive variant that can only reach the g-code through printer_extruder_variant.
// Both entries carry it so the assertion is independent of which physical extruder the
// emitted change routes through.
config.set_key_value("printer_extruder_variant",
new ConfigOptionStrings({"Direct Drive TPU High Flow", "Direct Drive TPU High Flow"}));
config.set_key_value("change_filament_gcode", new ConfigOptionString(
"; VARIANT old={old_extruder_variant} new={new_extruder_variant}\nT[next_filament_id]\n"));
WHEN("the print is sliced") {
const std::string gcode = slice_two_object_bbl(config);
THEN("both placeholders resolve to the printer_extruder_variant value") {
// The resolved line is the proof: an unresolved token or a parser throw would
// prevent this exact line from being emitted. (A negative "{token}" check is
// unreliable — the g-code's trailing config dump echoes the raw template.)
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring(
"; VARIANT old=Direct Drive TPU High Flow new=Direct Drive TPU High Flow"));
}
}
}
}
SCENARIO("Global current-tool placeholders resolve in a context with no local injection", "[GCodeWriter][H2C]")
{
GIVEN("a BBL dual-extruder print whose before_layer_change_gcode reads the current-tool placeholders") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
// before_layer_change is one of the contexts that inject NO current_* into their local config
// (unlike change_filament / machine_end / layer_change), so these placeholders can only resolve
// through the GLOBAL parser vars published at each toolchange (and at the initial set_extruder).
// Pre-fix current_filament_id / current_extruder_id / current_nozzle_id were undefined here and the
// whole slice threw a PlaceholderParserError — the same failure mode X2D's layer_change hit.
config.set_key_value("before_layer_change_gcode", new ConfigOptionString(
"; GVAR fid={current_filament_id} eid={current_extruder_id} nid={current_nozzle_id}\n"));
WHEN("the print is sliced (obj1 on filament 0, obj2 on filament 1)") {
std::string gcode;
REQUIRE_NOTHROW(gcode = slice_two_object_bbl(config));
THEN("all three globals resolve to the CORRECT active-tool values on both sides of the change") {
// Assert the FULL resolved marker, not just no-throw: obj1 prints on filament 0 (extruder 0,
// nozzle 0) and obj2 on filament 1 (extruder 1, nozzle 1). Locking every field means a
// stale or wrong global (e.g. obj2 still reading fid=0, or a mismatched extruder/nozzle id)
// fails here — this is the value guard that replaces the old "throws on undefined" canary.
// Only the emitted before_layer_change lines carry resolved values; the trailing config dump
// keeps the raw "{current_filament_id}" template, so these are unambiguous.
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("; GVAR fid=0 eid=0 nid=0"));
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("; GVAR fid=1 eid=1 nid=1"));
}
}
}
}
SCENARIO("Shipped dual-nozzle change_filament_gcode resolves during a real slice", "[GCodeWriter][H2C][Profiles]")
{
const std::string printer = GENERATE(std::string("H2C"), std::string("H2D"), std::string("H2D Pro"), std::string("X2D"));
GIVEN("the real " + printer + " change_filament_gcode driving a BBL dual-extruder slice") {
DynamicPrintConfig config = dual_extruder_toolchange_config();
config.set_key_value("change_filament_gcode", new ConfigOptionString(shipped_change_filament_gcode(printer)));
// H2D's gcode branches on the extruder variant; give the extruders distinct kits so the
// "Direct Drive TPU High Flow" branch is reachable.
config.set_key_value("printer_extruder_variant",
new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive TPU High Flow"}));
// Extruder-indexed machine rates the stock gcode divides by (default size 1); size to 2 extruders.
config.set_key_value("hotend_cooling_rate", new ConfigOptionFloatsNullable({2.0, 2.0}));
config.set_key_value("hotend_heating_rate", new ConfigOptionFloatsNullable({2.0, 2.0}));
THEN("every placeholder resolves (no undefined-variable throw) and the change block runs") {
std::string gcode;
REQUIRE_NOTHROW(gcode = slice_two_object_bbl(config));
// A resolved marker only the emitted change block produces (the trailing config
// dump keeps the raw "{filament_type[...]}" template), so this confirms the real
// change_filament_gcode was expanded, not merely echoed.
REQUIRE_THAT(gcode, Catch::Matchers::ContainsSubstring("set_filament_type:PLA"));
}
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/GCodeReader.hpp"
#include "test_helpers.hpp"
#include <cctype>
#include <set>
#include <string>
using namespace Slic3r;
using namespace Slic3r::Test;
// 0-based tool indices used by extrusions whose role comment contains `role` (needs gcode_comments).
static std::set<int> tools_for_role(const std::string& gcode, const std::string& role)
{
std::set<int> tools;
int current_tool = 0;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader& self, const GCodeReader::GCodeLine& line) {
const std::string cmd(line.cmd());
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char)cmd[1]))
current_tool = std::stoi(cmd.substr(1));
else if (line.extruding(self) && std::string(line.comment()).find(role) != std::string::npos)
tools.insert(current_tool);
});
return tools;
}
// Tool index = filament id - 1; brim and skirt follow the wall filament.
TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]")
{
auto [infill_filament, wall_filament] = GENERATE(table<int, int>({ {1, 1}, {1, 2}, {2, 1}, {2, 2} }));
DYNAMIC_SECTION("infill filament " << infill_filament << ", wall filament " << wall_filament) {
const std::string gcode = slice({ cube(20) },
multifilament_config(2, {
{ "sparse_infill_filament_id", infill_filament },
{ "internal_solid_filament_id", infill_filament },
{ "top_surface_filament_id", infill_filament },
{ "bottom_surface_filament_id", infill_filament },
{ "outer_wall_filament_id", wall_filament },
{ "inner_wall_filament_id", wall_filament },
{ "skirt_loops", 1 },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
}));
const std::set<int> wall_tool{ wall_filament - 1 };
const std::set<int> infill_tool{ infill_filament - 1 };
CHECK(tools_for_role(gcode, "perimeter") == wall_tool);
CHECK(tools_for_role(gcode, "infill") == infill_tool); // sparse + solid + top/bottom
CHECK(tools_for_role(gcode, "brim") == wall_tool);
CHECK(tools_for_role(gcode, "skirt") == wall_tool);
}
}
TEST_CASE("Each feature prints with its assigned filament (three filaments)", "[MultiFilament]")
{
const std::string gcode = slice({ cube(20) },
multifilament_config(3, {
{ "sparse_infill_filament_id", 2 },
{ "internal_solid_filament_id", 2 },
{ "top_surface_filament_id", 2 },
{ "bottom_surface_filament_id", 2 },
{ "outer_wall_filament_id", 3 },
{ "inner_wall_filament_id", 3 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
}));
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 2 }); // filament 3
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 1 }); // filament 2
}
// The override must survive tool ordering: object 1's walls print on their filament's
// tool, object 0 stays on the first. If dropped, every wall prints on tool 0.
TEST_CASE("Per-object wall filament override is honored", "[MultiFilament]")
{
const std::string gcode = slice_with_object_overrides(
{ cube(20), cube(20) },
multifilament_config(2, {
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
{ "print_sequence", "by object" },
}),
{ {}, { { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 } } });
CHECK(tools_for_role(gcode, "perimeter") == std::set<int>{ 0, 1 });
CHECK(tools_for_role(gcode, "infill") == std::set<int>{ 0 }); // infill not overridden: stays on F1
}
// max_layer_height can be shorter than the extruder count (normalization sizes it to the
// filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering
// indexed it per-nozzle and read past the end. Shortened directly here to isolate that read;
// the other per-extruder keys stay extruder-length so slicing reaches the code under test.
TEST_CASE("Multi-extruder slice stays in bounds with a short max_layer_height", "[MultiFilament]")
{
DynamicPrintConfig config = multifilament_config(2);
config.set_deserialize_strict({
{ "nozzle_diameter", "0.4,0.4" },
{ "printer_extruder_id", "1,2" },
{ "printer_extruder_variant", "Direct Drive Standard,Direct Drive Standard" },
{ "extruder_printable_height", "0,0" },
{ "max_layer_height", "0.3" }, // deliberately one entry short
});
Print print;
init_and_process_print({ cube(20) }, print, config);
REQUIRE_FALSE(print.objects().front()->layers().empty());
}

View File

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

View File

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

View File

@@ -3,17 +3,21 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/Print.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "test_data.hpp"
#include "test_helpers.hpp"
#include <iterator>
#include <set>
using namespace Slic3r;
using namespace Slic3r::Test;
SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
SCENARIO("Object layer heights", "[PrintObject]") {
GIVEN("A 20mm cube") {
WHEN("sliced with a 2mm layer height and a 3mm nozzle") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 },
{ "layer_height", 2 },
{ "nozzle_diameter", 3 }
@@ -32,7 +36,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
}
WHEN("sliced with a 10mm layer height and an 11mm nozzle") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 },
{ "layer_height", 10 },
{ "nozzle_diameter", 11 }
@@ -50,7 +54,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
}
WHEN("sliced with a 15mm layer height and a 16mm nozzle") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 2 },
{ "layer_height", 15 },
{ "nozzle_diameter", 16 }
@@ -71,7 +75,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
// rejects the slice during flow computation. Pin that behavior.
THEN("Slicing is rejected") {
Slic3r::Print print;
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "initial_layer_print_height", 0.3 },
{ "layer_height", 0.5 },
{ "nozzle_diameter", 0.4 }
@@ -81,11 +85,11 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
}
}
SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
SCENARIO("Perimeter generation", "[PrintObject]") {
GIVEN("20mm cube and default config") {
WHEN("make_perimeters() is called") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { { "sparse_infill_density", 0 } });
Slic3r::Test::init_and_process_print({cube(20)}, print, { { "sparse_infill_density", 0 } });
const PrintObject &object = *print.objects().front();
THEN("Every layer in region 0 has 1 island of perimeters") {
for (const Layer *layer : object.layers())
@@ -94,7 +98,7 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
}
WHEN("wall_loops is set to 3") {
Slic3r::Print print;
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
Slic3r::Test::init_and_process_print({cube(20)}, print, {
{ "sparse_infill_density", 0 },
{ "wall_loops", 3 }
});
@@ -106,3 +110,23 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
}
}
}
TEST_CASE("Initial layer height is honored", "[PrintObject]")
{
const std::string gcode = Slic3r::Test::slice({cube(20)}, {
{ "initial_layer_print_height", 0.3 },
{ "layer_height", 0.2 },
{ "z_hop", 0 } // keep recorded Z equal to the printed layer height
});
std::set<double> layer_zs;
GCodeReader reader;
reader.parse_buffer(gcode, [&layer_zs] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
if (line.extruding(self) && line.dist_XY(self) > 0)
layer_zs.insert(self.z());
});
REQUIRE(layer_zs.size() > 1);
REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4));
REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4));
}

View File

@@ -9,190 +9,319 @@
#include <cmath>
#include "test_data.hpp" // get access to init_print, etc
#include "test_helpers.hpp" // get access to init_print, etc
using namespace Slic3r::Test;
using namespace Slic3r;
/// Helper method to find the tool used for the brim (always the first extrusion).
[[maybe_unused]] static int get_brim_tool(const std::string &gcode)
// Distinct brim regions (combine_brims merges touching brims into one covering >1 object).
static int brim_count(const Print &print)
{
int brim_tool = -1;
int tool = -1;
GCodeReader parser;
parser.parse_buffer(gcode, [&tool, &brim_tool] (Slic3r::GCodeReader &self, const Slic3r::GCodeReader::GCodeLine &line)
{
// if the command is a T command, set the current tool
if (boost::starts_with(line.cmd(), "T")) {
tool = atoi(line.cmd().data() + 1);
} else if (line.cmd() == "G1" && line.extruding(self) && line.dist_XY(self) > 0 && brim_tool < 0) {
brim_tool = tool;
int n = 0;
for (const auto &group : print.skirt_brim_groups())
n += (int) group.brims.size();
return n;
}
// Total brim loops across all objects.
static size_t brim_loop_count(Print &print)
{
size_t n = 0;
for (const auto &kv : print.get_brimMap())
n += kv.second.items_count();
return n;
}
// The span is skirt_height layers, or every layer when a draft shield is on (forced even at
// height 0); per-object skirts are rejected in By object printing (no room between objects).
TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")
{
const int object_layers = 100; // 20mm cube at 0.2mm layers
const char *skirt_type = GENERATE("combined", "perobject");
const char *print_seq = GENERATE("by layer", "by object");
const char *draft_shield = GENERATE("disabled", "enabled");
const int skirt_height = GENERATE(0, 1, 3);
DYNAMIC_SECTION(skirt_type << " | " << print_seq << " | draft=" << draft_shield << " | height=" << skirt_height) {
auto do_slice = [&] {
return slice_two_cubes_arranged({
{ "skirt_loops", 1 },
{ "skirt_height", skirt_height },
{ "skirt_distance", 3 },
{ "skirt_type", skirt_type },
{ "draft_shield", draft_shield },
{ "print_sequence", print_seq },
{ "layer_height", 0.2 },
});
};
const bool draft = std::string(draft_shield) == "enabled";
const bool has_skirt = draft || skirt_height > 0;
const bool unsafe_by_object = std::string(skirt_type) == "perobject"
&& std::string(print_seq) == "by object" && has_skirt;
if (unsafe_by_object) {
REQUIRE_THROWS(do_slice());
} else {
const int expected_layers = draft ? object_layers : skirt_height;
CHECK(role_passes(do_slice(), "skirt") == expected_layers);
}
}
}
// Each per-object skirt prints right before its own object, so distant objects yield two
// non-contiguous skirt passes; close objects group into a single skirt.
TEST_CASE("Per-object skirts group when objects are close", "[SkirtBrim]")
{
auto [gap, expected_skirts] = GENERATE(table<double, int>({ { 5.0, 1 }, { 60.0, 2 } }));
DYNAMIC_SECTION("gap=" << gap) {
const std::string gcode = slice_two_cubes_apart(gap, {
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "layer_height", 0.2 },
});
CHECK(role_passes(gcode, "skirt") == expected_skirts);
}
}
TEST_CASE("Combine brims merges touching brims", "[SkirtBrim]")
{
auto [gap, combine, expected_brims] = GENERATE(table<double, int, int>({
{ 5.0, 1, 1 }, // touching + combine -> one merged brim
{ 5.0, 0, 2 }, // touching, no combine -> separate
{ 60.0, 1, 2 }, // far apart -> nothing to merge
}));
DYNAMIC_SECTION("gap=" << gap << " combine_brims=" << combine) {
Print print;
Model model;
place_two_cubes_apart(gap, {
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
{ "combine_brims", combine },
{ "layer_height", 0.2 },
}, print, model);
print.process();
CHECK(brim_count(print) == expected_brims);
}
}
// Each object's skirt and brim come right before that object, not all skirts then all brims first.
TEST_CASE("By-layer per-object skirt and brim precede each object", "[SkirtBrim]")
{
const std::string gcode = slice_two_cubes_apart(60, { // far apart: a skirt+brim per object
{ "skirt_loops", 1 },
{ "skirt_height", 1 },
{ "skirt_distance", 3 },
{ "skirt_type", "perobject" },
{ "print_sequence", "by layer" },
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
{ "layer_height", 0.2 },
});
const std::vector<std::string> expected{ "skirt", "brim", "perimeter", "skirt", "brim", "perimeter" };
CHECK(role_sequence(gcode, { "skirt", "brim", "perimeter" }) == expected);
}
// A square's corners are 90 degrees, so they get ears only when brim_ears_max_angle is above 90.
TEST_CASE("Brim ears appear only at corners within the max angle", "[SkirtBrim]")
{
auto [max_angle, expect_ears] = GENERATE(table<int, bool>({ { 91, true }, { 90, false }, { 89, false } }));
DYNAMIC_SECTION("brim_ears_max_angle=" << max_angle) {
Print print;
init_and_process_print({ cube(20) }, print, {
{ "skirt_loops", 0 },
{ "brim_type", "brim_ears" },
{ "brim_width", 1 },
{ "brim_ears_max_angle", max_angle },
{ "initial_layer_line_width", 0.5 },
});
if (expect_ears) CHECK(brim_loop_count(print) > 0);
else CHECK(brim_loop_count(print) == 0);
}
}
SCENARIO("Skirt has the configured number of loops", "[SkirtBrim]") {
GIVEN("20mm cube and default config") {
WHEN("skirt_loops is set to 2") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "skirt_height", 1 },
{ "skirt_distance", 1 },
{ "skirt_loops", 2 }
});
THEN("Skirt Extrusion collection has 2 loops in it") {
REQUIRE(print.skirt().items_count() == 2);
REQUIRE(print.skirt().flatten().entities.size() == 2);
}
}
}
}
SCENARIO("Brim has the configured number of loops", "[SkirtBrim]") {
GIVEN("20mm cube and default config, 1mm first layer width") {
WHEN("Brim is set to 6mm") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "brim_type", "outer_only" },
{ "initial_layer_line_width", 1 },
{ "brim_width", 6 }
});
THEN("Brim Extrusion collection has 6 loops in it") {
REQUIRE(brim_loop_count(print) == 6);
}
}
WHEN("Brim is set to 6mm, extrusion width 0.5mm") {
Print print;
init_and_process_print({cube(20)}, print, {
{ "brim_type", "outer_only" },
{ "brim_width", 6 },
{ "initial_layer_line_width", 0.5 }
});
THEN("Brim Extrusion collection has 12 loops in it") {
REQUIRE(brim_loop_count(print) == 12);
}
}
}
}
static double first_extrusion_feedrate_for_feature(const std::string &gcode, const std::string_view feature)
{
double feedrate = 0.0;
bool feature_active = false;
GCodeReader parser;
parser.parse_buffer(gcode, [&feedrate, &feature_active, feature] (GCodeReader &self, const GCodeReader::GCodeLine &line) {
const std::string_view comment = line.comment();
if (comment.find("FEATURE:") != std::string_view::npos || comment.find("TYPE:") != std::string_view::npos)
feature_active = comment.find(feature) != std::string_view::npos;
if (feature_active && line.extruding(self) && line.dist_XY(self) > 0) {
feedrate = line.new_F(self);
self.quit_parsing();
}
});
return brim_tool;
return feedrate;
}
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"));
}
TEST_CASE("Brim uses first layer speed", "[SkirtBrim]") {
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "brim_type", "outer_only" },
{ "brim_width", 5 },
{ "gcode_comments", true },
{ "initial_layer_speed", 10 },
{ "initial_layer_infill_speed", 20 },
{ "machine_start_gcode", "" },
{ "skirt_loops", 0 },
{ "slow_down_for_layer_cooling", false },
{ "z_hop", 0 }
});
const std::string gcode = Slic3r::Test::slice({cube(20)}, config);
const double brim_feedrate = first_extrusion_feedrate_for_feature(gcode, "Brim");
REQUIRE(brim_feedrate > 0.0);
REQUIRE_THAT(brim_feedrate, Catch::Matchers::WithinAbs(600.0, 1e-3));
const double bottom_surface_feedrate = first_extrusion_feedrate_for_feature(gcode, "Bottom surface");
REQUIRE(bottom_surface_feedrate > 0.0);
REQUIRE_THAT(bottom_surface_feedrate, Catch::Matchers::WithinAbs(1200.0, 1e-3));
}
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 +329,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 +343,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);
});

View File

@@ -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);
@@ -93,3 +93,14 @@ SCENARIO("SupportMaterial: support_layers_z and contact_distance", "[SupportMate
}
}
}
// extrude_support once held a `static` lambda capturing `this`, so a second export in the
// same process dereferenced a returned stack frame (ASan: stack-use-after-return).
TEST_CASE("Support G-code emission survives a second slice in the same process", "[SupportMaterial][Regression]")
{
const std::string first = slice({ TestMesh::overhang }, { { "enable_support", 1 } });
REQUIRE(! layers_with_role(first, "support").empty());
const std::string second = slice({ TestMesh::overhang }, { { "enable_support", 1 } });
REQUIRE(! layers_with_role(second, "support").empty());
}

View File

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