mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-31 14:52:06 +00:00
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:
@@ -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
93
tests/fff_print/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# fff_print test suite
|
||||
|
||||
Component- and pipeline-level tests for FFF slicing: the path from a `Model` plus config, through `Print` / `PrintObject`, to emitted G-code.
|
||||
|
||||
For Catch2 mechanics (assertions, generators, matchers, random ordering, thread-safety), see [../CLAUDE.md](../CLAUDE.md). This document is the organizing contract for the suite: where a test goes, and how it is named.
|
||||
|
||||
## Organizing principle
|
||||
|
||||
**One file per subsystem. A subsystem is usually a single production class (`Flow`, `PrintObject`), but may be a cohesive feature that spans several (skirt/brim lives in `Brim.cpp`, `Print.cpp`, and `GCode.cpp`). That file owns every test for the subsystem: in-memory-state assertions and emitted-G-code assertions alike.**
|
||||
|
||||
A test's home is decided by *what production code it exercises*, never by *how it observes the result*. A skirt test that inspects `print.skirt()` and one that greps the G-code for `; skirt` live in the same file.
|
||||
|
||||
If you touched code in a subsystem, its test file is where your test goes. If a subsystem has no file yet, add `test_<subsystem>.cpp` and list it in `CMakeLists.txt`.
|
||||
|
||||
## File ownership
|
||||
|
||||
### Building blocks (one class, exercised through its API)
|
||||
|
||||
| File | Source (`src/libslic3r/`) | Covers |
|
||||
|---|---|---|
|
||||
| `test_trianglemesh` | `TriangleMesh.{c,h}pp` | mesh stats, transforms, slicing, split/merge/cut |
|
||||
| `test_flow` | `Flow.{c,h}pp` | extrusion width / area math |
|
||||
| `test_extrusion_entity` | `ExtrusionEntity.{c,h}pp` | extrusion-collection geometry |
|
||||
| `test_gcodewriter` | `GCodeWriter.{c,h}pp`, `GCode.cpp` | low-level G-code emit primitives, origin |
|
||||
| `test_model` | `Model.{c,h}pp` | object / volume / instance construction |
|
||||
|
||||
### Slicing pipeline (build a `Print`, then assert state or G-code)
|
||||
|
||||
| File | Source (`src/libslic3r/`) | Covers |
|
||||
|---|---|---|
|
||||
| `test_printobject` | `PrintObject.cpp` | layer heights, perimeter generation |
|
||||
| `test_fill` | `Fill/` | infill patterns and infill G-code |
|
||||
| `test_skirt_brim` | `Brim.cpp`, `Print.cpp` | skirt/brim loop counts, grouping, brim ears, emission order |
|
||||
| `test_support_material` | `Support/` | support & raft layers, contact distance |
|
||||
| `test_cooling` | `GCode/CoolingBuffer.cpp` | fan control, speed-marker consumption |
|
||||
| `test_multifilament` | `GCode/ToolOrdering.cpp` | per-feature and per-object filament routing |
|
||||
| `test_print` | `Print.{c,h}pp` | `validate()`, solid-shell behavior, sequential printing, custom G-code & config comments, default-slice smoke |
|
||||
|
||||
Paths are under `src/libslic3r/`. A trailing `/` is a directory of related files; otherwise it is a single class. `{c,h}pp` means the `.cpp`/`.hpp` pair.
|
||||
|
||||
## Naming and tags
|
||||
|
||||
- **File:** `test_<subsystem>.cpp`.
|
||||
- **Test name:** a plain behavioral sentence, present tense, stating the contract the test pins down. No `Subsystem:` prefix (the tag carries that).
|
||||
- Good: `TEST_CASE("Skirt is emitted once per layer it spans", "[SkirtBrim]")`
|
||||
- Avoid: `TEST_CASE("Print: Skirt generation", "[Print]")`
|
||||
- **Tags:**
|
||||
- Exactly one **subsystem** tag, PascalCase, matching the file (`[SkirtBrim]`, `[PrintObject]`, `[Fill]`). This is the grouping / filter key.
|
||||
- Optional **cross-cutting** tags for a concern that genuinely spans files (`[validate]`, `[Regression]`).
|
||||
- **Status** tags: `[NotWorking]` marks a test disabled for a known, documented reason; CI excludes it via `~[NotWorking]` (it does not hide itself). Use `[.]` to hide a test from default runs entirely. Either way, say why in a one-line comment.
|
||||
|
||||
## Test style
|
||||
|
||||
Prefer a flat `TEST_CASE` per behavior, with `GENERATE` for parameterized cases and shared setup factored into helpers. The test name carries the behavior, so the BDD scaffolding is usually redundant. Reserve `SCENARIO` / `GIVEN` / `WHEN` / `THEN` for a test with genuine shared setup that branches into a few closely related variations, and never let a `SCENARIO` accumulate unrelated `WHEN`s: that grab-bag is what this contract exists to prevent (and it hides failures behind a single coarse test case).
|
||||
|
||||
## Robust tests
|
||||
|
||||
A test should fail only when the behavior it names breaks, not from unrelated changes (the "change-detector" anti-pattern). Test behavior, not incidentals, and aim for one reason to fail. Concretely:
|
||||
|
||||
- Don't depend on or assert defaults: set the config keys the behavior needs, and derive expected values from those inputs (a 20mm cube at 0.2mm = 100 layers), not from a default that may change.
|
||||
- Assert the defining property, not an incidental value: prefer "skirt present", "at least 2 brim loops", or "ears vs none" over exact coordinates, extrusion amounts, line counts, or byte sizes.
|
||||
- Compare floats with a tolerance (`WithinAbs` / `WithinRel`), never `==`.
|
||||
- Match the meaningful G-code token (`; skirt`), not whole lines, whitespace, or comment wording.
|
||||
- Rely on ordering only when it is the contract (as `role_sequence` does).
|
||||
- Keep tests self-contained: no shared state, green under `--order rand`.
|
||||
|
||||
## Helpers
|
||||
|
||||
Reuse these instead of building a `Print` or parsing G-code by hand.
|
||||
|
||||
- **Global** (`tests/test_utils.hpp`, available to every suite):
|
||||
- `load_model("file.obj")`: load a `TriangleMesh` from `tests/data/`.
|
||||
- `ScopedTemporaryFile`: an RAII temp-file path, removed on scope exit.
|
||||
- **Suite harness** (`fff_print/test_helpers.{hpp,cpp}`):
|
||||
- Build and run: `init_print(...)`, `init_and_process_print(...)`, `slice(...)` (returns the G-code string), and `gcode(print)`.
|
||||
- Two-cube placement: `slice_two_cubes_arranged(...)` (arranger-positioned), and `place_two_cubes_apart(...)` / `slice_two_cubes_apart(...)` (a fixed gap, not arranged).
|
||||
- Meshes: `cube(size)` / `make_cube(...)` for simple shapes; the `TestMesh` enum with `mesh(...)` for named fixtures.
|
||||
- G-code analysis: `layers_with_role(gcode, role)`, `max_z(gcode)`, `role_passes(gcode, role)`, `role_sequence(gcode, roles)`. Subsystem-specific checks stay local (for example `brim_count` in `test_skirt_brim`).
|
||||
|
||||
Promote a helper into the suite harness when it is a general test primitive (not tied to one subsystem's logic), even if only one file uses it today; keep genuinely subsystem-specific helpers local (file-static). Reuse potential, not current usage count, is the test.
|
||||
|
||||
## Adding a test (checklist)
|
||||
|
||||
1. Find the subsystem's file in the tables; create `test_<subsystem>.cpp` if missing.
|
||||
2. Build the print with a harness helper; set only the config keys the behavior needs.
|
||||
3. Assert the behavior, in-memory or via parsed G-code, whichever is clearest.
|
||||
4. Name it as a behavioral sentence and tag it `[Subsystem]`.
|
||||
5. For a bug fix, add the regression test in the owning file. Name it for the behavior it protects; the test must stand on its own without relying on an external issue or PR for meaning.
|
||||
|
||||
## Running
|
||||
|
||||
ctest --test-dir build/tests/fff_print
|
||||
build/tests/fff_print/<config>/fff_print_tests --order rand "~[NotWorking]"
|
||||
27
tests/fff_print/test_cooling.cpp
Normal file
27
tests/fff_print/test_cooling.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
// The fan is held off for the first close_fan_the_first_x_layers layers, so an explicit
|
||||
// fan-off command is emitted.
|
||||
TEST_CASE("Fan is held off for the initial layers", "[Cooling]")
|
||||
{
|
||||
const std::string gcode = slice({ cube(20) }, {
|
||||
{ "cooling", true },
|
||||
{ "close_fan_the_first_x_layers", 5 },
|
||||
});
|
||||
CHECK(gcode.find("M106 S0") != std::string::npos);
|
||||
}
|
||||
|
||||
// The cooling pass resolves and strips its internal speed placeholders; none leak into
|
||||
// the final G-code.
|
||||
TEST_CASE("Cooling consumes its internal speed markers", "[Cooling]")
|
||||
{
|
||||
const std::string gcode = slice({ cube(20) }, { { "layer_height", 0.2 } });
|
||||
CHECK(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
#ifndef SLIC3R_TEST_DATA_HPP
|
||||
#define SLIC3R_TEST_DATA_HPP
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Slic3r { namespace Test {
|
||||
|
||||
constexpr double MM_PER_MIN = 60.0;
|
||||
|
||||
/// Enumeration of test meshes
|
||||
enum class TestMesh {
|
||||
A,
|
||||
L,
|
||||
V,
|
||||
_40x10,
|
||||
cube_20x20x20,
|
||||
sphere_50mm,
|
||||
bridge,
|
||||
bridge_with_hole,
|
||||
cube_with_concave_hole,
|
||||
cube_with_hole,
|
||||
gt2_teeth,
|
||||
ipadstand,
|
||||
overhang,
|
||||
pyramid,
|
||||
sloping_hole,
|
||||
slopy_cube,
|
||||
small_dorito,
|
||||
step,
|
||||
two_hollow_squares
|
||||
};
|
||||
|
||||
// Necessary for <c++17
|
||||
struct TestMeshHash {
|
||||
std::size_t operator()(TestMesh tm) const {
|
||||
return static_cast<std::size_t>(tm);
|
||||
}
|
||||
};
|
||||
|
||||
/// Mesh enumeration to name mapping
|
||||
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
|
||||
|
||||
/// Port of Slic3r::Test::mesh
|
||||
/// Basic cubes/boxes should call TriangleMesh::make_cube() directly and rescale/translate it
|
||||
TriangleMesh mesh(TestMesh m);
|
||||
|
||||
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
|
||||
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
|
||||
|
||||
/// Templated function to see if two values are equivalent (+/- epsilon)
|
||||
template <typename T>
|
||||
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
|
||||
|
||||
template <typename T>
|
||||
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
|
||||
|
||||
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
|
||||
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model& model, const DynamicPrintConfig &config_in, bool comments = false);
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config(), bool comments = false);
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model& model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config, bool comments = false);
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
|
||||
std::string gcode(Print& print);
|
||||
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments = false);
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments = false);
|
||||
|
||||
// Distinct layer Z heights that carry an extrusion tagged with the given role
|
||||
// comment (requires gcode_comments), e.g. "skirt", "brim", "support".
|
||||
std::set<double> layers_with_role(const std::string &gcode, const std::string &role);
|
||||
|
||||
// Highest Z reached by any move in the gcode.
|
||||
double max_z(const std::string &gcode);
|
||||
|
||||
} } // namespace Slic3r::Test
|
||||
|
||||
|
||||
#endif // SLIC3R_TEST_DATA_HPP
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
@@ -35,7 +35,7 @@ static Slic3r::ExtrusionPaths random_paths(size_t count = 10, size_t length = 20
|
||||
return p;
|
||||
}
|
||||
|
||||
SCENARIO("ExtrusionEntityCollection: Polygon flattening", "[ExtrusionEntity]") {
|
||||
SCENARIO("Polygon flattening", "[ExtrusionEntity]") {
|
||||
srand(0xDEADBEEF); // consistent seed for test reproducibility.
|
||||
|
||||
// Generate one specific random path set and save it for later comparison
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
#include "libslic3r/SVG.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
bool test_if_solid_surface_filled(const ExPolygon& expolygon, double flow_spacing, double angle = 0, double density = 1.0);
|
||||
|
||||
#if 0
|
||||
TEST_CASE("Fill: adjusted solid distance") {
|
||||
TEST_CASE("Adjusted solid distance", "[Fill]") {
|
||||
int surface_width = 250;
|
||||
int distance = Slic3r::Flow::solid_spacing(surface_width, 47);
|
||||
REQUIRE(distance == Catch::Approx(50));
|
||||
@@ -26,7 +26,7 @@ TEST_CASE("Fill: adjusted solid distance") {
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_CASE("Fill: Pattern Path Length", "[Fill]") {
|
||||
TEST_CASE("Pattern path length", "[Fill]") {
|
||||
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("rectilinear"));
|
||||
filler->angle = float(-(PI)/2.0);
|
||||
FillParams fill_params;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <numeric>
|
||||
#include <sstream>
|
||||
|
||||
#include "test_data.hpp" // get access to init_print, etc
|
||||
#include "test_helpers.hpp" // get access to init_print, etc
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
@@ -17,7 +17,7 @@ using namespace Slic3r;
|
||||
|
||||
/// Test the expected behavior for auto-width,
|
||||
/// spacing, etc
|
||||
SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
|
||||
SCENARIO("Flow math for non-bridges", "[Flow]") {
|
||||
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
|
||||
ConfigOptionFloatOrPercent width(1.0, false);
|
||||
float nozzle_diameter = 0.4f;
|
||||
@@ -79,7 +79,7 @@ SCENARIO("Flow: Flow math for non-bridges", "[Flow]") {
|
||||
}
|
||||
|
||||
/// Spacing, width calculation for bridge extrusions
|
||||
SCENARIO("Flow: Flow math for bridges", "[Flow]") {
|
||||
SCENARIO("Flow math for bridges", "[Flow]") {
|
||||
GIVEN("Nozzle Diameter of 0.4, a desired width of 1mm and layer height of 0.5") {
|
||||
float nozzle_diameter = 0.4f;
|
||||
float bridge_flow = 1.0f;
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "libslic3r/GCode.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
SCENARIO("Origin manipulation", "[GCode]") {
|
||||
Slic3r::GCode gcodegen;
|
||||
WHEN("set_origin to (10,0)") {
|
||||
gcodegen.set_origin(Vec2d(10,0));
|
||||
REQUIRE(gcodegen.origin() == Vec2d(10, 0));
|
||||
}
|
||||
WHEN("set_origin to (10,0) and translate by (5, 5)") {
|
||||
gcodegen.set_origin(Vec2d(10,0));
|
||||
gcodegen.set_origin(gcodegen.origin() + Vec2d(5, 5));
|
||||
THEN("origin returns reference to point") {
|
||||
REQUIRE(gcodegen.origin() == Vec2d(15,5));
|
||||
}
|
||||
}
|
||||
}
|
||||
420
tests/fff_print/test_gcode_timing.cpp
Normal file
420
tests/fff_print/test_gcode_timing.cpp
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
@@ -19,13 +19,11 @@ using namespace std;
|
||||
|
||||
namespace Slic3r { namespace Test {
|
||||
|
||||
// Mesh enumeration to name mapping
|
||||
const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names {
|
||||
std::pair<TestMesh, const char*>(TestMesh::A, "A"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::L, "L"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::V, "V"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::_40x10, "40x10"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::cube_20x20x20, "cube_20x20x20"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::sphere_50mm, "sphere_50mm"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::bridge, "bridge"),
|
||||
std::pair<TestMesh, const char*>(TestMesh::bridge_with_hole, "bridge_with_hole"),
|
||||
@@ -46,9 +44,6 @@ TriangleMesh mesh(TestMesh m)
|
||||
{
|
||||
TriangleMesh mesh;
|
||||
switch(m) {
|
||||
case TestMesh::cube_20x20x20:
|
||||
mesh = Slic3r::make_cube(20, 20, 20);
|
||||
break;
|
||||
case TestMesh::sphere_50mm:
|
||||
mesh = Slic3r::make_sphere(50, PI / 243.0);
|
||||
break;
|
||||
@@ -187,30 +182,72 @@ TriangleMesh mesh(TestMesh m)
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static bool verbose_gcode()
|
||||
Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh)
|
||||
{
|
||||
const char *v = std::getenv("SLIC3R_TESTS_GCODE");
|
||||
if (v == nullptr)
|
||||
return false;
|
||||
std::string s(v);
|
||||
return s == "1" || s == "on" || s == "yes";
|
||||
Slic3r::Model result;
|
||||
ModelObject *object = result.add_object();
|
||||
object->name += model_name + ".stl";
|
||||
object->add_volume(_mesh);
|
||||
object->add_instance();
|
||||
return result;
|
||||
}
|
||||
|
||||
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments)
|
||||
DynamicPrintConfig multifilament_config(unsigned int filaments, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra)
|
||||
{
|
||||
// Single nozzle, `filaments` filaments. Colours must be DISTINCT: filament grouping
|
||||
// treats same-colour filaments as one and the tool-order path then drops/segfaults.
|
||||
static const char *palette[] = { "#FF0000", "#00FF00", "#0000FF", "#FFFF00",
|
||||
"#FF00FF", "#00FFFF", "#FF8000", "#8000FF" };
|
||||
std::string diameters, colours, flush;
|
||||
for (unsigned int i = 0; i < filaments; ++i) {
|
||||
diameters += (i ? "," : "") + std::string("1.75");
|
||||
colours += (i ? ";" : "") + std::string(palette[i % (sizeof(palette) / sizeof(palette[0]))]);
|
||||
for (unsigned int j = 0; j < filaments; ++j)
|
||||
flush += ((i || j) ? "," : "") + std::string(i == j ? "0" : "280");
|
||||
}
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({ { "nozzle_diameter", "0.4" }, { "filament_diameter", diameters } });
|
||||
config.set_num_filaments(filaments);
|
||||
|
||||
// These are read by filament id during export but absent from filament_option_keys(),
|
||||
// so set_num_filaments leaves them size 1; size them too, else out-of-range access.
|
||||
const auto &defaults = FullPrintConfig::defaults();
|
||||
for (const char *key : { "filament_type", "filament_vendor", "filament_start_gcode" })
|
||||
static_cast<ConfigOptionVectorBase *>(config.option(key, true))->resize(filaments, defaults.option(key));
|
||||
|
||||
// flush_volumes_matrix must be sized filaments*filaments or export rejects it.
|
||||
config.set_deserialize_strict({ { "filament_colour", colours }, { "flush_volumes_matrix", flush } });
|
||||
|
||||
if (extra.size() > 0)
|
||||
config.set_deserialize_strict(extra);
|
||||
return config;
|
||||
}
|
||||
|
||||
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in,
|
||||
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> *per_object_overrides, bool arrange)
|
||||
{
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.apply(config_in);
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
|
||||
if (verbose_gcode())
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
|
||||
size_t object_idx = 0;
|
||||
for (const TriangleMesh &t : meshes) {
|
||||
ModelObject *object = model.add_object();
|
||||
object->name += "object.stl";
|
||||
object->add_volume(std::move(t));
|
||||
object->add_instance();
|
||||
|
||||
if (per_object_overrides && object_idx < per_object_overrides->size() && !(*per_object_overrides)[object_idx].empty()) {
|
||||
DynamicPrintConfig oc;
|
||||
for (const auto &item : (*per_object_overrides)[object_idx])
|
||||
oc.set_deserialize_strict(item.opt_key, item.opt_value);
|
||||
object->config.apply(oc);
|
||||
}
|
||||
++object_idx;
|
||||
}
|
||||
arrange_objects(model, InfiniteBed{}, ArrangeParams{ scaled(min_object_distance(config))});
|
||||
if (arrange)
|
||||
arrange_objects(model, InfiniteBed{}, ArrangeParams{ scaled(min_object_distance(config))});
|
||||
for (ModelObject *mo : model.objects) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
@@ -221,63 +258,63 @@ void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r
|
||||
print.set_status_silent();
|
||||
}
|
||||
|
||||
void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in, bool comments)
|
||||
void init_print(std::initializer_list<TestMesh> test_meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in)
|
||||
{
|
||||
std::vector<TriangleMesh> triangle_meshes;
|
||||
triangle_meshes.reserve(test_meshes.size());
|
||||
for (const TestMesh test_mesh : test_meshes)
|
||||
triangle_meshes.emplace_back(mesh(test_mesh));
|
||||
init_print(std::move(triangle_meshes), print, model, config_in, comments);
|
||||
init_print(std::move(triangle_meshes), print, model, config_in);
|
||||
}
|
||||
|
||||
void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in, bool comments)
|
||||
void init_print(std::initializer_list<TriangleMesh> input_meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in)
|
||||
{
|
||||
std::vector<TriangleMesh> triangle_meshes;
|
||||
triangle_meshes.reserve(input_meshes.size());
|
||||
for (const TriangleMesh &input_mesh : input_meshes)
|
||||
triangle_meshes.emplace_back(input_mesh);
|
||||
init_print(std::move(triangle_meshes), print, model, config_in, comments);
|
||||
init_print(std::move(triangle_meshes), print, model, config_in);
|
||||
}
|
||||
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict(config_items);
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
}
|
||||
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict(config_items);
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
}
|
||||
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments)
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config)
|
||||
{
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
print.process();
|
||||
}
|
||||
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config, bool comments)
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig &config)
|
||||
{
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
init_print(meshes, print, model, config);
|
||||
print.process();
|
||||
}
|
||||
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
init_print(meshes, print, model, config_items);
|
||||
print.process();
|
||||
}
|
||||
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
init_print(meshes, print, model, config_items);
|
||||
print.process();
|
||||
}
|
||||
|
||||
@@ -292,6 +329,76 @@ std::string gcode(Print & print)
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice_with_object_overrides(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config,
|
||||
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> &per_object_overrides)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(std::vector<TriangleMesh>(meshes), print, model, config, &per_object_overrides);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice_two_cubes_arranged(std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
return slice({ cube(20), cube(20) }, config_items);
|
||||
}
|
||||
|
||||
void place_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
|
||||
Print &print, Model &model)
|
||||
{
|
||||
TriangleMesh a = cube(20);
|
||||
a.translate(80, 80, 0);
|
||||
TriangleMesh b = cube(20);
|
||||
b.translate(80 + 20 + gap, 80, 0);
|
||||
std::vector<TriangleMesh> meshes;
|
||||
meshes.push_back(std::move(a));
|
||||
meshes.push_back(std::move(b));
|
||||
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict(config_items);
|
||||
init_print(std::move(meshes), print, model, config, nullptr, /*arrange=*/false);
|
||||
}
|
||||
|
||||
std::string slice_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items)
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
place_two_cubes_apart(gap, config_items, print, model);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::set<double> layers_with_role(const std::string &gcode, const std::string &role)
|
||||
{
|
||||
std::set<double> layers;
|
||||
@@ -313,59 +420,48 @@ double max_z(const std::string &gcode)
|
||||
return z;
|
||||
}
|
||||
|
||||
Slic3r::Model model(const std::string &model_name, TriangleMesh &&_mesh)
|
||||
int role_passes(const std::string &gcode, const std::string &role)
|
||||
{
|
||||
Slic3r::Model result;
|
||||
ModelObject *object = result.add_object();
|
||||
object->name += model_name + ".stl";
|
||||
object->add_volume(_mesh);
|
||||
object->add_instance();
|
||||
return result;
|
||||
int passes = 0;
|
||||
bool in_role = false;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
if (! line.extruding(self)) return;
|
||||
const bool is_role = line.comment().find(role) != std::string_view::npos;
|
||||
if (is_role && ! in_role) ++passes;
|
||||
in_role = is_role;
|
||||
});
|
||||
return passes;
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config, bool comments)
|
||||
std::vector<std::string> role_sequence(const std::string &gcode, const std::vector<std::string> &roles)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config, bool comments)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config, comments);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items, bool comments)
|
||||
{
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
init_print(meshes, print, model, config_items, comments);
|
||||
return gcode(print);
|
||||
std::vector<std::string> seq;
|
||||
std::string current;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
if (! line.extruding(self)) return;
|
||||
const std::string_view comment = line.comment();
|
||||
for (const std::string &role : roles)
|
||||
if (comment.find(role) != std::string_view::npos) {
|
||||
if (current != role) { seq.push_back(role); current = role; }
|
||||
break;
|
||||
}
|
||||
});
|
||||
return seq;
|
||||
}
|
||||
|
||||
} } // namespace Slic3r::Test
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
SCENARIO("init_print functionality", "[test_data]") {
|
||||
SCENARIO("init_print functionality", "[test_helpers]") {
|
||||
GIVEN("A default config") {
|
||||
Slic3r::DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
WHEN("init_print is called with a single mesh.") {
|
||||
Slic3r::Model model;
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_print({ Slic3r::Test::TestMesh::cube_20x20x20 }, print, model, config, true);
|
||||
Slic3r::Test::init_print({ Slic3r::Test::cube(20) }, print, model, config);
|
||||
THEN("One mesh/printobject is in the resulting Print object.") {
|
||||
REQUIRE(print.objects().size() == 1);
|
||||
}
|
||||
125
tests/fff_print/test_helpers.hpp
Normal file
125
tests/fff_print/test_helpers.hpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#ifndef SLIC3R_TEST_HELPERS_HPP
|
||||
#define SLIC3R_TEST_HELPERS_HPP
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r { namespace Test {
|
||||
|
||||
constexpr double MM_PER_MIN = 60.0;
|
||||
|
||||
// True when `a` and `b` are within EPSILON.
|
||||
template <typename T>
|
||||
bool _equiv(const T& a, const T& b) { return std::abs(a - b) < EPSILON; }
|
||||
|
||||
// True when `a` and `b` are within `epsilon`.
|
||||
template <typename T>
|
||||
bool _equiv(const T& a, const T& b, double epsilon) { return abs(a - b) < epsilon; }
|
||||
|
||||
// Named reusable test meshes, resolved by mesh().
|
||||
enum class TestMesh {
|
||||
A,
|
||||
L,
|
||||
V,
|
||||
_40x10,
|
||||
sphere_50mm,
|
||||
bridge,
|
||||
bridge_with_hole,
|
||||
cube_with_concave_hole,
|
||||
cube_with_hole,
|
||||
gt2_teeth,
|
||||
ipadstand,
|
||||
overhang,
|
||||
pyramid,
|
||||
sloping_hole,
|
||||
slopy_cube,
|
||||
small_dorito,
|
||||
step,
|
||||
two_hollow_squares
|
||||
};
|
||||
|
||||
// Hash for TestMesh (std::hash lacks scoped-enum support before C++17).
|
||||
struct TestMeshHash {
|
||||
std::size_t operator()(TestMesh tm) const {
|
||||
return static_cast<std::size_t>(tm);
|
||||
}
|
||||
};
|
||||
|
||||
// TestMesh value to name mapping.
|
||||
extern const std::unordered_map<TestMesh, const char*, TestMeshHash> mesh_names;
|
||||
|
||||
// Geometry for the named test fixture `m`, optionally translated and scaled.
|
||||
TriangleMesh mesh(TestMesh m);
|
||||
TriangleMesh mesh(TestMesh m, Vec3d translate, Vec3d scale = Vec3d(1.0, 1.0, 1.0));
|
||||
TriangleMesh mesh(TestMesh m, Vec3d translate, double scale = 1.0);
|
||||
|
||||
// An equal-sided cube, `size` mm on each edge.
|
||||
inline TriangleMesh cube(double size) { return make_cube(size, size, size); }
|
||||
|
||||
// A Model holding one object built from `mesh`.
|
||||
Slic3r::Model model(const std::string& model_name, TriangleMesh&& _mesh);
|
||||
|
||||
// Single-nozzle, `filaments`-filament config from defaults; `extra` is applied last.
|
||||
DynamicPrintConfig multifilament_config(unsigned int filaments,
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {});
|
||||
|
||||
// Apply `meshes` and config to `print`/`model`; optional per-object overrides, auto-arranged unless `arrange` is false.
|
||||
void init_print(std::vector<TriangleMesh> &&meshes, Slic3r::Print &print, Slic3r::Model &model, const DynamicPrintConfig &config_in,
|
||||
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> *per_object_overrides = nullptr, bool arrange = true);
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config());
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, const Slic3r::DynamicPrintConfig &config_in = Slic3r::DynamicPrintConfig::full_print_config());
|
||||
void init_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
void init_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, Slic3r::Model &model, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
|
||||
// init_print followed by process(), leaving a sliced `print` to inspect.
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config);
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, const DynamicPrintConfig& config);
|
||||
void init_and_process_print(std::initializer_list<TestMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
void init_and_process_print(std::initializer_list<TriangleMesh> meshes, Slic3r::Print &print, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
|
||||
// Process `print` and return its exported G-code.
|
||||
std::string gcode(Print& print);
|
||||
|
||||
// Build, slice, and return the G-code for `meshes` under the given config.
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, const DynamicPrintConfig &config);
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config);
|
||||
std::string slice(std::initializer_list<TestMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
std::string slice(std::initializer_list<TriangleMesh> meshes, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
|
||||
// Slice `meshes`, applying per_object_overrides[i] to object i first (empty entry = none).
|
||||
std::string slice_with_object_overrides(std::initializer_list<TriangleMesh> meshes, const DynamicPrintConfig &config,
|
||||
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> &per_object_overrides);
|
||||
|
||||
// Slice two auto-arranged 20mm cubes (the arranger positions them).
|
||||
std::string slice_two_cubes_arranged(std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
|
||||
// Place two 20mm cubes `gap` mm apart edge-to-edge, not auto-arranged (the caller controls spacing).
|
||||
void place_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items,
|
||||
Slic3r::Print &print, Slic3r::Model &model);
|
||||
// Slice two 20mm cubes `gap` mm apart (not auto-arranged) and return the G-code.
|
||||
std::string slice_two_cubes_apart(double gap, std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> config_items);
|
||||
|
||||
// Distinct layer Z heights carrying an extrusion of the given `role` (e.g. "skirt").
|
||||
std::set<double> layers_with_role(const std::string &gcode, const std::string &role);
|
||||
|
||||
// Highest Z reached by any move in the G-code.
|
||||
double max_z(const std::string &gcode);
|
||||
|
||||
// Count of contiguous extrusion blocks of `role` (each uninterrupted run counts once).
|
||||
int role_passes(const std::string &gcode, const std::string &role);
|
||||
|
||||
// The `roles` in the order their extrusion blocks first appear, consecutive repeats collapsed.
|
||||
std::vector<std::string> role_sequence(const std::string &gcode, const std::vector<std::string> &roles);
|
||||
|
||||
} } // namespace Slic3r::Test
|
||||
|
||||
#endif // SLIC3R_TEST_HELPERS_HPP
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
106
tests/fff_print/test_multifilament.cpp
Normal file
106
tests/fff_print/test_multifilament.cpp
Normal 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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,629 +0,0 @@
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <boost/regex.hpp>
|
||||
#include <libslic3r/ModelArrange.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
boost::regex perimeters_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; perimeter");
|
||||
boost::regex infill_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; infill");
|
||||
boost::regex skirt_regex("G1 X[-0-9.]* Y[-0-9.]* E[-0-9.]* ; skirt");
|
||||
|
||||
SCENARIO( "PrintGCode basic functionality", "[PrintGCode]") {
|
||||
GIVEN("A default configuration and a print test object") {
|
||||
WHEN("the output is executed with no support material") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model, {
|
||||
{ "layer_height", 0.2 },
|
||||
{ "initial_layer_print_height", 0.2 },
|
||||
{ "initial_layer_line_width", 0 },
|
||||
{ "gcode_comments", true },
|
||||
{ "machine_start_gcode", "" },
|
||||
{ "z_hop", 0 }
|
||||
});
|
||||
std::string gcode = Slic3r::Test::gcode(print);
|
||||
THEN("Some text output is generated.") {
|
||||
REQUIRE(gcode.size() > 0);
|
||||
}
|
||||
//THEN("Exported text contains git commit id") {
|
||||
// REQUIRE(gcode.find("; Git Commit") != std::string::npos);
|
||||
// REQUIRE(gcode.find(SLIC3R_BUILD_ID) != std::string::npos);
|
||||
//}
|
||||
THEN("Exported text contains extrusion statistics.") {
|
||||
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
|
||||
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
|
||||
}
|
||||
THEN("Exported text does not contain cooling markers (they were consumed)") {
|
||||
REQUIRE(gcode.find(";_EXTRUDE_SET_SPEED") == std::string::npos);
|
||||
}
|
||||
|
||||
THEN("The config trailer includes print and region settings") {
|
||||
REQUIRE(gcode.find("; layer_height") != std::string::npos);
|
||||
REQUIRE(gcode.find("; sparse_infill_density") != std::string::npos);
|
||||
}
|
||||
THEN("Infill is emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
|
||||
}
|
||||
THEN("Perimeters are emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
|
||||
}
|
||||
THEN("Skirt is emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
|
||||
}
|
||||
THEN("final Z height is 20mm") {
|
||||
REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20., 1e-4));
|
||||
}
|
||||
}
|
||||
WHEN("output is executed with two objects printed sequentially") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Model model;
|
||||
Slic3r::Test::init_print({TestMesh::cube_20x20x20,TestMesh::cube_20x20x20}, print, model, {
|
||||
{ "initial_layer_line_width", 0 },
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "layer_height", 0.2 },
|
||||
{ "enable_support", false },
|
||||
{ "raft_layers", 0 },
|
||||
{ "print_sequence", "by object" },
|
||||
{ "gcode_comments", true },
|
||||
{ "printing_by_object_gcode", "; between-object-gcode" },
|
||||
{ "z_hop", 0 }
|
||||
});
|
||||
std::string gcode = Slic3r::Test::gcode(print);
|
||||
THEN("Some text output is generated.") {
|
||||
REQUIRE(gcode.size() > 0);
|
||||
}
|
||||
THEN("Infill is emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, infill_regex));
|
||||
}
|
||||
THEN("Perimeters are emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, perimeters_regex));
|
||||
}
|
||||
THEN("Skirt is emitted.") {
|
||||
boost::smatch has_match;
|
||||
REQUIRE(boost::regex_search(gcode, has_match, skirt_regex));
|
||||
}
|
||||
THEN("Between-object-gcode is emitted.") {
|
||||
REQUIRE(gcode.find("; between-object-gcode") != std::string::npos);
|
||||
}
|
||||
THEN("final Z height is 20.1mm") {
|
||||
REQUIRE_THAT(max_z(gcode), Catch::Matchers::WithinAbs(20.1, 1e-4));
|
||||
}
|
||||
THEN("Z height resets on object change") {
|
||||
double final_z = 0.0;
|
||||
bool reset = false;
|
||||
GCodeReader reader;
|
||||
reader.apply_config(print.config());
|
||||
reader.parse_buffer(gcode, [&final_z, &reset] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
if (final_z > 0 && std::abs(self.z() - 0.3) < 0.01 ) { // saw higher Z before this, now it's lower
|
||||
reset = true;
|
||||
} else {
|
||||
final_z = std::max(final_z, static_cast<double>(self.z())); // record the highest Z point we reach
|
||||
}
|
||||
});
|
||||
REQUIRE(reset == true);
|
||||
}
|
||||
}
|
||||
WHEN("the output is executed with support material") {
|
||||
std::string gcode = ::Test::slice({TestMesh::cube_20x20x20}, {
|
||||
{ "initial_layer_line_width", 0 },
|
||||
{ "enable_support", true },
|
||||
{ "raft_layers", 3 },
|
||||
{ "gcode_comments", true }
|
||||
});
|
||||
THEN("Some text output is generated.") {
|
||||
REQUIRE(gcode.size() > 0);
|
||||
}
|
||||
THEN("Exported text contains extrusion statistics.") {
|
||||
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; support material extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; first layer extrusion width") == std::string::npos);
|
||||
}
|
||||
THEN("Raft is emitted.") {
|
||||
REQUIRE(gcode.find("; raft") != std::string::npos);
|
||||
}
|
||||
}
|
||||
WHEN("the output is executed with a separate first layer extrusion width") {
|
||||
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
|
||||
{ "initial_layer_line_width", "0.5" }
|
||||
});
|
||||
THEN("Some text output is generated.") {
|
||||
REQUIRE(gcode.size() > 0);
|
||||
}
|
||||
THEN("Exported text contains extrusion statistics.") {
|
||||
REQUIRE(gcode.find("; external perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; perimeters extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; solid infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; top infill extrusion width") != std::string::npos);
|
||||
REQUIRE(gcode.find("; support material extrusion width") == std::string::npos);
|
||||
REQUIRE(gcode.find("; first layer extrusion width") != std::string::npos);
|
||||
}
|
||||
}
|
||||
WHEN("Cooling is enabled and the fan is disabled.") {
|
||||
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
|
||||
{ "cooling", true },
|
||||
{ "close_fan_the_first_x_layers", 5 }
|
||||
});
|
||||
THEN("GCode to disable fan is emitted."){
|
||||
REQUIRE(gcode.find("M106 S0") != std::string::npos);
|
||||
}
|
||||
}
|
||||
WHEN("end_gcode exists with layer_num and layer_z") {
|
||||
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
|
||||
{ "machine_end_gcode", "; Layer_num [layer_num]\n; Layer_z [layer_z]" },
|
||||
{ "layer_height", 0.1 },
|
||||
{ "initial_layer_print_height", 0.1 }
|
||||
});
|
||||
THEN("layer_num and layer_z are processed in the end gcode") {
|
||||
REQUIRE(gcode.find("; Layer_num 199") != std::string::npos);
|
||||
REQUIRE(gcode.find("; Layer_z 20") != std::string::npos);
|
||||
}
|
||||
}
|
||||
WHEN("current_extruder exists in start_gcode") {
|
||||
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20 }, {
|
||||
{ "machine_start_gcode", "; Extruder [current_extruder]" }
|
||||
});
|
||||
THEN("current_extruder is processed in the start gcode and set for first extruder") {
|
||||
REQUIRE(gcode.find("; Extruder 0") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
WHEN("layer_num represents the layer's index from z=0") {
|
||||
std::string gcode = ::Test::slice({ TestMesh::cube_20x20x20, TestMesh::cube_20x20x20 }, {
|
||||
{ "print_sequence", "by object" },
|
||||
{ "gcode_comments", true },
|
||||
{ "layer_change_gcode", ";Layer:[layer_num] ([layer_z] mm)" },
|
||||
{ "layer_height", 0.1 },
|
||||
{ "initial_layer_print_height", 0.1 }
|
||||
});
|
||||
// End of the 1st object.
|
||||
std::string token = ";Layer:199 ";
|
||||
size_t pos = gcode.find(token);
|
||||
THEN("First and second object last layer is emitted") {
|
||||
// First object
|
||||
REQUIRE(pos != std::string::npos);
|
||||
pos += token.size();
|
||||
REQUIRE(pos < gcode.size());
|
||||
double z = 0;
|
||||
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
|
||||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(20., 1e-4));
|
||||
// Second object
|
||||
pos = gcode.find(";Layer:399 ", pos);
|
||||
REQUIRE(pos != std::string::npos);
|
||||
pos += token.size();
|
||||
REQUIRE(pos < gcode.size());
|
||||
REQUIRE((sscanf(gcode.data() + pos, "(%lf mm)", &z) == 1));
|
||||
REQUIRE_THAT(z, Catch::Matchers::WithinAbs(20., 1e-4));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("export_gcode writes G-code without a result pointer", "[PrintGCode][export_gcode]")
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
Slic3r::Test::init_print({TestMesh::cube_20x20x20}, print, model);
|
||||
print.process();
|
||||
|
||||
SECTION("non-BBL printer") {}
|
||||
SECTION("BBL printer") { print.is_BBL_printer() = true; }
|
||||
|
||||
ScopedTemporaryFile temp(".gcode");
|
||||
REQUIRE_NOTHROW(print.export_gcode(temp.string(), nullptr, nullptr));
|
||||
|
||||
std::ifstream in(temp.string());
|
||||
const std::string gcode((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
|
||||
|
||||
REQUIRE_FALSE(gcode.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Initial layer height is honored", "[PrintGCode]")
|
||||
{
|
||||
const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, {
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "layer_height", 0.2 },
|
||||
{ "z_hop", 0 } // keep recorded Z equal to the printed layer height
|
||||
});
|
||||
|
||||
std::set<double> layer_zs;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&layer_zs] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
if (line.extruding(self) && line.dist_XY(self) > 0)
|
||||
layer_zs.insert(self.z());
|
||||
});
|
||||
|
||||
REQUIRE(layer_zs.size() > 1);
|
||||
REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4));
|
||||
REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("Sequential printing follows model order", "[PrintGCode]")
|
||||
{
|
||||
// Two objects of different heights, taller one added first. Orca prints
|
||||
// sequential objects in model order, so the taller one is printed first.
|
||||
const std::string gcode = Slic3r::Test::slice({ Slic3r::make_cube(20, 20, 20), Slic3r::make_cube(20, 20, 10) }, {
|
||||
{ "print_sequence", "by object" },
|
||||
{ "layer_height", 0.2 },
|
||||
{ "initial_layer_print_height", 0.2 },
|
||||
{ "z_hop", 0 }
|
||||
});
|
||||
|
||||
// The first object's height is the peak Z reached before Z drops back to the
|
||||
// first layer (the object change). With by-object printing only an object
|
||||
// change returns Z to the bottom.
|
||||
double first_object_peak_z = 0.0;
|
||||
double running_peak = 0.0;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
if (first_object_peak_z != 0.0 || !line.extruding(self)) return; // ignore travels (e.g. start-gcode Z lift)
|
||||
if (running_peak > 1.0 && self.z() < 1.0)
|
||||
first_object_peak_z = running_peak;
|
||||
else
|
||||
running_peak = std::max(running_peak, static_cast<double>(self.z()));
|
||||
});
|
||||
|
||||
REQUIRE_THAT(first_object_peak_z, Catch::Matchers::WithinAbs(20.0, 0.3));
|
||||
}
|
||||
|
||||
// Verify that emit_machine_limits_to_gcode emits the correct max value across
|
||||
// used extruders (regression for commit b4ee665: "Emit max value of machine
|
||||
// limit among used extruders").
|
||||
TEST_CASE("Machine envelope emits max limit among used extruders", "[PrintGCode][MachineEnvelope]")
|
||||
{
|
||||
SECTION("Single extruder emits its configured values") {
|
||||
const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, {
|
||||
{ "emit_machine_limits_to_gcode", "1" },
|
||||
{ "gcode_flavor", "marlin2" },
|
||||
{ "gcode_comments", "1" },
|
||||
{ "machine_start_gcode", "" },
|
||||
{ "layer_height", "0.2" },
|
||||
{ "initial_layer_print_height", "0.2" },
|
||||
{ "initial_layer_line_width", "0" },
|
||||
{ "z_hop", "0" },
|
||||
// stride-2 options: (normal, silent)
|
||||
{ "machine_max_acceleration_x", "500,600" },
|
||||
{ "machine_max_acceleration_y", "700,800" },
|
||||
{ "machine_max_acceleration_z", "100,200" },
|
||||
{ "machine_max_acceleration_e", "5000,6000" },
|
||||
{ "machine_max_acceleration_extruding", "1200,1300" },
|
||||
{ "machine_max_acceleration_retracting", "1400,1500" },
|
||||
{ "machine_max_acceleration_travel", "1600,1700" },
|
||||
// stride-2 options: (normal, silent)
|
||||
{ "machine_max_speed_x", "100,100" },
|
||||
{ "machine_max_speed_y", "110,110" },
|
||||
{ "machine_max_speed_z", "10,10" },
|
||||
{ "machine_max_speed_e", "50,50" },
|
||||
{ "machine_max_jerk_x", "8,8" },
|
||||
{ "machine_max_jerk_y", "9,9" },
|
||||
{ "machine_max_jerk_z", "0.4,0.4" },
|
||||
{ "machine_max_jerk_e", "5,5" },
|
||||
{ "machine_max_junction_deviation", "0.02,0.03" },
|
||||
});
|
||||
|
||||
THEN("M201 uses the normal acceleration values") {
|
||||
REQUIRE(gcode.find("M201 X500 Y700 Z100 E5000") != std::string::npos);
|
||||
}
|
||||
THEN("M203 uses the speed values") {
|
||||
REQUIRE(gcode.find("M203 X100 Y110 Z10 E50") != std::string::npos);
|
||||
}
|
||||
THEN("M204 (Marlin 2) uses extruding / retracting / travel") {
|
||||
REQUIRE(gcode.find("M204 P1200 R1400 T1600") != std::string::npos);
|
||||
}
|
||||
THEN("M205 uses the jerk values") {
|
||||
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
|
||||
}
|
||||
THEN("M205 J uses the junction deviation") {
|
||||
REQUIRE(gcode.find("M205 J0.020") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Legacy Marlin flavor emits correct format") {
|
||||
const std::string gcode = Slic3r::Test::slice({TestMesh::cube_20x20x20}, {
|
||||
{ "emit_machine_limits_to_gcode", "1" },
|
||||
{ "gcode_flavor", "marlin" },
|
||||
{ "gcode_comments", "1" },
|
||||
{ "machine_start_gcode", "" },
|
||||
{ "layer_height", "0.2" },
|
||||
{ "initial_layer_print_height", "0.2" },
|
||||
{ "initial_layer_line_width", "0" },
|
||||
{ "z_hop", "0" },
|
||||
// All machine limits must be provided — defaults are empty vectors.
|
||||
{ "machine_max_acceleration_x", "500,600" },
|
||||
{ "machine_max_acceleration_y", "500,600" },
|
||||
{ "machine_max_acceleration_z", "500,600" },
|
||||
{ "machine_max_acceleration_e", "5000,6000" },
|
||||
{ "machine_max_acceleration_extruding", "1200,1300" },
|
||||
{ "machine_max_acceleration_retracting", "1400,1500" },
|
||||
{ "machine_max_acceleration_travel", "1600,1700" },
|
||||
{ "machine_max_speed_x", "100,100" },
|
||||
{ "machine_max_speed_y", "110,110" },
|
||||
{ "machine_max_speed_z", "10,10" },
|
||||
{ "machine_max_speed_e", "50,50" },
|
||||
{ "machine_max_jerk_x", "8,8" },
|
||||
{ "machine_max_jerk_y", "9,9" },
|
||||
{ "machine_max_jerk_z", "0.4,0.4" },
|
||||
{ "machine_max_jerk_e", "5,5" },
|
||||
{ "machine_max_junction_deviation", "0.02,0.03" },
|
||||
});
|
||||
|
||||
THEN("Legacy Marlin: M204 travel_acc = extruding_acc") {
|
||||
// gcfMarlinLegacy uses extruding acc for travel too
|
||||
REQUIRE(gcode.find("M204 P1200 R1400 T1200") != std::string::npos);
|
||||
}
|
||||
THEN("Legacy Marlin: M205 uses mm/sec format") {
|
||||
REQUIRE(gcode.find("M205 X8.00 Y9.00 Z0.40 E5.00") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Multi extruder - max of used extruders is emitted") {
|
||||
// Build config with 2 extruders that have *different* machine limits.
|
||||
// Extruder 1 has higher values; the emitted G-code must use the max.
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
// Print basics
|
||||
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
|
||||
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
|
||||
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
|
||||
// Print objects sequentially so each uses its own extruder without
|
||||
// wipe-tower / tool-change complexity.
|
||||
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
|
||||
// 2 extruders
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
|
||||
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
|
||||
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
|
||||
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
|
||||
// filament_map maps filament slot index (1-based) → logical extruder ID (1-based).
|
||||
// Default [1] maps everything to extruder 0. Need [1, 2] for two distinct extruders.
|
||||
// fmmManual prevents auto-computation from overwriting the explicit mapping.
|
||||
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
|
||||
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
|
||||
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
|
||||
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
|
||||
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
|
||||
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
|
||||
// flush_volumes_matrix must be filament_count^2 * heads_count entries.
|
||||
// 2 filaments * 2 * 1 head = 4 entries (all zero — flush volumes not tested here).
|
||||
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
|
||||
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
|
||||
|
||||
// Machine limits: extruder 0 low, extruder 1 high
|
||||
// Stride-2 (normal, silent pairs): e0_n, e0_s, e1_n, e1_s
|
||||
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({700, 0, 1100, 0}));
|
||||
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 300, 0}));
|
||||
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 8000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({1200, 0, 2200, 0}));
|
||||
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({1400, 0, 2400, 0}));
|
||||
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({1600, 0, 2600, 0}));
|
||||
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
|
||||
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
|
||||
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
|
||||
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
|
||||
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({8, 0, 12, 0}));
|
||||
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({9, 0, 13, 0}));
|
||||
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.6, 0}));
|
||||
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({5, 0, 10, 0}));
|
||||
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.05, 0}));
|
||||
|
||||
// Model: two objects assigned to different extruders
|
||||
Model model;
|
||||
auto* obj1 = model.add_object();
|
||||
obj1->add_volume(mesh(TestMesh::cube_20x20x20));
|
||||
obj1->add_instance();
|
||||
// obj1 uses default extruder=1 (0-based index 0)
|
||||
|
||||
auto* obj2 = model.add_object();
|
||||
obj2->add_volume(mesh(TestMesh::cube_20x20x20));
|
||||
obj2->add_instance();
|
||||
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
|
||||
|
||||
Print print;
|
||||
arrange_objects(model, InfiniteBed{},
|
||||
ArrangeParams{scaled(min_object_distance(config))});
|
||||
for (auto* mo : model.objects) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
}
|
||||
|
||||
print.apply(model, config);
|
||||
print.validate();
|
||||
print.set_status_silent();
|
||||
print.process();
|
||||
|
||||
std::string gcode = Slic3r::Test::gcode(print);
|
||||
|
||||
THEN("M201 contains max (extruder 1's) acceleration values") {
|
||||
REQUIRE(gcode.find("M201 X1000 Y1100 Z300 E8000") != std::string::npos);
|
||||
}
|
||||
THEN("M203 contains max speed values") {
|
||||
REQUIRE(gcode.find("M203 X200 Y210 Z30 E80") != std::string::npos);
|
||||
}
|
||||
THEN("M204 contains max extruding / retracting / travel") {
|
||||
REQUIRE(gcode.find("M204 P2200 R2400 T2600") != std::string::npos);
|
||||
}
|
||||
THEN("M205 contains max jerk values") {
|
||||
REQUIRE(gcode.find("M205 X12.00 Y13.00 Z0.60 E10.00") != std::string::npos);
|
||||
}
|
||||
THEN("M205 contains max m_max_junction_deviation ") {
|
||||
REQUIRE(gcode.find("M205 J0.050") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that the EXTRUDER_LIMIT macro (GCodeWriter.cpp) correctly:
|
||||
// 1) Uses the active extruder's specific limit when filament() is known.
|
||||
// 2) Falls back to the maximum of all extruder limits when filament() is nullptr.
|
||||
//
|
||||
// These two behaviours were introduced in:
|
||||
// - "Use per-extruder motion limit" (1ab34a7454)
|
||||
// - "Use max limit when current extruder is unknown" (b7240ab1c6)
|
||||
TEST_CASE("EXTRUDER_LIMIT per-extruder clamping and max fallback", "[PrintGCode][MachineEnvelope]")
|
||||
{
|
||||
// --- Build config with 2 extruders that have different machine limits ---
|
||||
// Extruder 0: low limits
|
||||
// Extruder 1: high limits
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
|
||||
config.set_key_value("emit_machine_limits_to_gcode", new ConfigOptionBool(true));
|
||||
config.set_key_value("gcode_flavor", new ConfigOptionEnum<GCodeFlavor>(gcfMarlinFirmware));
|
||||
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
|
||||
config.set_key_value("machine_start_gcode", new ConfigOptionString(""));
|
||||
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
|
||||
config.set_key_value("initial_layer_line_width", new ConfigOptionFloatOrPercent(0, false));
|
||||
config.set_key_value("z_hop", new ConfigOptionFloats({0}));
|
||||
config.set_key_value("print_sequence", new ConfigOptionEnum<PrintSequence>(PrintSequence::ByObject));
|
||||
|
||||
// 2 extruders, 2 filaments
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
config.set_key_value("printer_extruder_id", new ConfigOptionInts({1, 2}));
|
||||
config.set_key_value("printer_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard"}));
|
||||
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
|
||||
config.set_key_value("filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
|
||||
config.set_key_value("filament_type", new ConfigOptionStrings({"PLA", "PLA"}));
|
||||
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
|
||||
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
|
||||
config.set_key_value("default_filament_colour", new ConfigOptionStrings({"#FF0000", "#00FF00"}));
|
||||
config.set_key_value("nozzle_temperature", new ConfigOptionInts({210, 210}));
|
||||
config.set_key_value("nozzle_temperature_range_low", new ConfigOptionInts({190, 190}));
|
||||
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
|
||||
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
|
||||
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 0, 0, 0}));
|
||||
|
||||
// --- Machine limits (stride-2: e0_n, e0_s, e1_n, e1_s) ---
|
||||
// Extruder 0 has LOW limits, Extruder 1 has HIGH limits.
|
||||
config.set_key_value("machine_max_acceleration_x", new ConfigOptionFloats({500, 0, 1000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_y", new ConfigOptionFloats({500, 0, 1000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_z", new ConfigOptionFloats({100, 0, 200, 0}));
|
||||
config.set_key_value("machine_max_acceleration_e", new ConfigOptionFloats({5000, 0, 5000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_extruding", new ConfigOptionFloats({500, 0, 2000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_retracting", new ConfigOptionFloats({600, 0, 2000, 0}));
|
||||
config.set_key_value("machine_max_acceleration_travel", new ConfigOptionFloats({700, 0, 2500, 0}));
|
||||
config.set_key_value("machine_max_speed_x", new ConfigOptionFloats({100, 0, 200, 0}));
|
||||
config.set_key_value("machine_max_speed_y", new ConfigOptionFloats({110, 0, 210, 0}));
|
||||
config.set_key_value("machine_max_speed_z", new ConfigOptionFloats({10, 0, 30, 0}));
|
||||
config.set_key_value("machine_max_speed_e", new ConfigOptionFloats({50, 0, 80, 0}));
|
||||
config.set_key_value("machine_max_jerk_x", new ConfigOptionFloats({5, 0, 15, 0}));
|
||||
config.set_key_value("machine_max_jerk_y", new ConfigOptionFloats({6, 0, 16, 0}));
|
||||
config.set_key_value("machine_max_jerk_z", new ConfigOptionFloats({0.4, 0, 0.8, 0}));
|
||||
config.set_key_value("machine_max_jerk_e", new ConfigOptionFloats({3, 0, 8, 0}));
|
||||
config.set_key_value("machine_max_junction_deviation", new ConfigOptionFloats({0.02, 0, 0.08, 0}));
|
||||
|
||||
// --- Print acceleration: 1500 mm/s² ---
|
||||
// Exceeds extruder 0's limit (500) → should be clamped to 500.
|
||||
// Does NOT exceed extruder 1's limit (2000) → passes through as 1500.
|
||||
config.set_key_value("default_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
config.set_key_value("outer_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
config.set_key_value("inner_wall_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
config.set_key_value("top_surface_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
config.set_key_value("initial_layer_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
config.set_key_value("travel_acceleration", new ConfigOptionFloats({1500, 1500}));
|
||||
|
||||
// Model: two objects assigned to different extruders
|
||||
Model model;
|
||||
auto* obj1 = model.add_object();
|
||||
obj1->add_volume(mesh(TestMesh::cube_20x20x20));
|
||||
obj1->add_instance();
|
||||
|
||||
auto* obj2 = model.add_object();
|
||||
obj2->add_volume(mesh(TestMesh::cube_20x20x20));
|
||||
obj2->add_instance();
|
||||
obj2->config.set_key_value("extruder", new ConfigOptionInt(2)); // 0-based index 1
|
||||
|
||||
Print print;
|
||||
arrange_objects(model, InfiniteBed{}, ArrangeParams{scaled(min_object_distance(config))});
|
||||
for (auto* mo : model.objects) {
|
||||
mo->ensure_on_bed();
|
||||
print.auto_assign_extruders(mo);
|
||||
}
|
||||
|
||||
print.apply(model, config);
|
||||
print.validate();
|
||||
print.set_status_silent();
|
||||
print.process();
|
||||
|
||||
std::string gcode = Slic3r::Test::gcode(print);
|
||||
|
||||
SECTION("Preamble: max limit among used extruders") {
|
||||
THEN("M201 uses max (extruder 1's) acceleration values") {
|
||||
REQUIRE(gcode.find("M201 X1000 Y1000 Z200 E5000") != std::string::npos);
|
||||
}
|
||||
THEN("M204 uses max extruding/retracting/travel") {
|
||||
REQUIRE(gcode.find("M204 P2000 R2000 T2500") != std::string::npos);
|
||||
}
|
||||
THEN("M205 uses max jerk values") {
|
||||
REQUIRE(gcode.find("M205 X15.00 Y16.00 Z0.80 E8.00") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Preamble: EXTRUDER_LIMIT falls back to max when no filament is active") {
|
||||
// set_junction_deviation() is called during preamble with no active filament.
|
||||
// EXTRUDER_LIMIT(m_max_junction_deviation) → filament() == nullptr → max of all (0.08).
|
||||
THEN("M205 J uses max junction deviation") {
|
||||
REQUIRE(gcode.find("M205 J0.080") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Print: extruder 0 acceleration clamped to its specific limit") {
|
||||
// Extruder 0 machine limit = 500. Print accel = 1500 > 500 → clamped to 500.
|
||||
THEN("M204 P500 appears (extruder 0 clamped)") {
|
||||
REQUIRE(gcode.find("M204 P500") != std::string::npos);
|
||||
}
|
||||
THEN("M204 T700 appears (extruder 0 travel clamped)") {
|
||||
REQUIRE(gcode.find("M204 T700") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Print: extruder 1 acceleration NOT clamped to extruder 0's limit") {
|
||||
// Extruder 1 machine limit = 2000. Print accel = 1500 < 2000 → not clamped.
|
||||
THEN("M204 P1500 appears (extruder 1 not clamped to 500)") {
|
||||
REQUIRE(gcode.find("M204 P1500") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,17 +3,21 @@
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include <iterator>
|
||||
#include <set>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
SCENARIO("Object layer heights", "[PrintObject]") {
|
||||
GIVEN("A 20mm cube") {
|
||||
WHEN("sliced with a 2mm layer height and a 3mm nozzle") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 2 },
|
||||
{ "layer_height", 2 },
|
||||
{ "nozzle_diameter", 3 }
|
||||
@@ -32,7 +36,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
}
|
||||
WHEN("sliced with a 10mm layer height and an 11mm nozzle") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 2 },
|
||||
{ "layer_height", 10 },
|
||||
{ "nozzle_diameter", 11 }
|
||||
@@ -50,7 +54,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
}
|
||||
WHEN("sliced with a 15mm layer height and a 16mm nozzle") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 2 },
|
||||
{ "layer_height", 15 },
|
||||
{ "nozzle_diameter", 16 }
|
||||
@@ -71,7 +75,7 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
// rejects the slice during flow computation. Pin that behavior.
|
||||
THEN("Slicing is rejected") {
|
||||
Slic3r::Print print;
|
||||
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
REQUIRE_THROWS(Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "layer_height", 0.5 },
|
||||
{ "nozzle_diameter", 0.4 }
|
||||
@@ -81,11 +85,11 @@ SCENARIO("PrintObject: object layer heights", "[PrintObject]") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
|
||||
SCENARIO("Perimeter generation", "[PrintObject]") {
|
||||
GIVEN("20mm cube and default config") {
|
||||
WHEN("make_perimeters() is called") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, { { "sparse_infill_density", 0 } });
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, { { "sparse_infill_density", 0 } });
|
||||
const PrintObject &object = *print.objects().front();
|
||||
THEN("Every layer in region 0 has 1 island of perimeters") {
|
||||
for (const Layer *layer : object.layers())
|
||||
@@ -94,7 +98,7 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
|
||||
}
|
||||
WHEN("wall_loops is set to 3") {
|
||||
Slic3r::Print print;
|
||||
Slic3r::Test::init_and_process_print({TestMesh::cube_20x20x20}, print, {
|
||||
Slic3r::Test::init_and_process_print({cube(20)}, print, {
|
||||
{ "sparse_infill_density", 0 },
|
||||
{ "wall_loops", 3 }
|
||||
});
|
||||
@@ -106,3 +110,23 @@ SCENARIO("PrintObject: Perimeter generation", "[PrintObject]") {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Initial layer height is honored", "[PrintObject]")
|
||||
{
|
||||
const std::string gcode = Slic3r::Test::slice({cube(20)}, {
|
||||
{ "initial_layer_print_height", 0.3 },
|
||||
{ "layer_height", 0.2 },
|
||||
{ "z_hop", 0 } // keep recorded Z equal to the printed layer height
|
||||
});
|
||||
|
||||
std::set<double> layer_zs;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&layer_zs] (GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
if (line.extruding(self) && line.dist_XY(self) > 0)
|
||||
layer_zs.insert(self.z());
|
||||
});
|
||||
|
||||
REQUIRE(layer_zs.size() > 1);
|
||||
REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4));
|
||||
REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4));
|
||||
}
|
||||
|
||||
@@ -9,190 +9,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);
|
||||
});
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
#include <chrono>
|
||||
|
||||
//#include "test_options.hpp"
|
||||
#include "test_data.hpp"
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace std;
|
||||
|
||||
static inline TriangleMesh make_cube() { return make_cube(20., 20, 20); }
|
||||
|
||||
SCENARIO( "TriangleMesh: Basic mesh statistics") {
|
||||
SCENARIO("Basic mesh statistics", "[TriangleMesh]") {
|
||||
GIVEN( "A 20mm cube, built from constexpr std::array" ) {
|
||||
std::vector<Vec3f> vertices { {20,20,0}, {20,0,0}, {0,0,0}, {0,20,0}, {20,20,20}, {0,20,20}, {0,0,20}, {20,0,20} };
|
||||
std::vector<Vec3i32> facets { {0,1,2}, {0,2,3}, {4,5,6}, {4,6,7}, {0,4,7}, {0,7,1}, {1,7,6}, {1,6,2}, {2,6,5}, {2,5,3}, {4,0,3}, {4,3,5} };
|
||||
@@ -71,7 +71,7 @@ SCENARIO( "TriangleMesh: Basic mesh statistics") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") {
|
||||
SCENARIO("Transformation functions affect the mesh as expected", "[TriangleMesh]") {
|
||||
GIVEN( "A 20mm cube with one corner on the origin") {
|
||||
auto cube = make_cube();
|
||||
|
||||
@@ -134,7 +134,7 @@ SCENARIO( "TriangleMesh: Transformation functions affect mesh as expected.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "TriangleMesh: slice behavior.") {
|
||||
SCENARIO("Slice behavior", "[TriangleMesh]") {
|
||||
GIVEN( "A 20mm cube with one corner on the origin") {
|
||||
auto cube = make_cube();
|
||||
|
||||
@@ -177,7 +177,7 @@ SCENARIO( "TriangleMesh: slice behavior.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "make_xxx functions produce meshes.") {
|
||||
SCENARIO("make_xxx functions produce meshes", "[TriangleMesh]") {
|
||||
GIVEN("make_cube() function") {
|
||||
WHEN("make_cube() is called with arguments 20,20,20") {
|
||||
TriangleMesh cube = make_cube(20,20,20);
|
||||
@@ -232,7 +232,7 @@ SCENARIO( "make_xxx functions produce meshes.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "TriangleMesh: split functionality.") {
|
||||
SCENARIO("Split functionality", "[TriangleMesh]") {
|
||||
GIVEN( "A 20mm cube with one corner on the origin") {
|
||||
auto cube = make_cube();
|
||||
WHEN( "The mesh is split into its component parts.") {
|
||||
@@ -260,7 +260,7 @@ SCENARIO( "TriangleMesh: split functionality.") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "TriangleMesh: Mesh merge functions") {
|
||||
SCENARIO("Mesh merge functions", "[TriangleMesh]") {
|
||||
GIVEN( "Two 20mm cubes, each with one corner on the origin") {
|
||||
auto cube = make_cube();
|
||||
TriangleMesh cube2(cube);
|
||||
@@ -274,7 +274,7 @@ SCENARIO( "TriangleMesh: Mesh merge functions") {
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
|
||||
SCENARIO("Cut behavior", "[TriangleMesh]") {
|
||||
GIVEN( "A 20mm cube with one corner on the origin") {
|
||||
auto cube = make_cube();
|
||||
WHEN( "Object is cut at the bottom") {
|
||||
@@ -302,7 +302,7 @@ SCENARIO( "TriangleMeshSlicer: Cut behavior.") {
|
||||
}
|
||||
}
|
||||
#ifdef TEST_PERFORMANCE
|
||||
TEST_CASE("Regression test for issue #4486 - files take forever to slice") {
|
||||
TEST_CASE("Large mesh slices within the time budget (#4486)", "[TriangleMesh][Regression]") {
|
||||
TriangleMesh mesh;
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/100_000.stl");
|
||||
@@ -329,7 +329,7 @@ TEST_CASE("Regression test for issue #4486 - files take forever to slice") {
|
||||
#endif // TEST_PERFORMANCE
|
||||
|
||||
#ifdef BUILD_PROFILE
|
||||
TEST_CASE("Profile test for issue #4486 - files take forever to slice") {
|
||||
TEST_CASE("Large mesh slicing profile (#4486)", "[TriangleMesh][Profile]") {
|
||||
TriangleMesh mesh;
|
||||
DynamicPrintConfig config = Slic3r::DynamicPrintConfig::full_print_config();
|
||||
mesh.ReadSTLFile(std::string(testfile_dir) + "test_trianglemesh/4486/10_000.stl");
|
||||
|
||||
Reference in New Issue
Block a user