mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 04:11:00 +00:00
Wipe tower sparse layers combination (#15841)
This commit is contained in:
@@ -308,6 +308,119 @@ TEST_CASE("A single-filament plate reserves a tower only when one is actually pr
|
||||
}
|
||||
}
|
||||
|
||||
// Filament 2 on the top surface only, so every layer below it is a toolchange-free tower layer: the
|
||||
// run "Combine sparse layers" folds. The two heights decide whether anything folds, so they are the
|
||||
// caller's business.
|
||||
static DynamicPrintConfig sparse_run_config(double layer_height, const char *max_layer_height, bool combine)
|
||||
{
|
||||
DynamicPrintConfig config = multifilament_config(2, {
|
||||
{ "top_surface_filament_id", 2 },
|
||||
{ "enable_prime_tower", true },
|
||||
{ "wipe_tower_x", 50 }, // inside the 200x200 test bed
|
||||
{ "wipe_tower_y", 50 },
|
||||
{ "prime_tower_width", 35 },
|
||||
{ "min_layer_height", "0.08"},
|
||||
{ "single_extruder_multi_material", true },
|
||||
{ "timelapse_type", "0" },
|
||||
{ "enable_wrapping_detection", false },
|
||||
{ "raft_layers", "0" } });
|
||||
// A taller first layer would top the plan and hide what the run does, so slice at one height.
|
||||
config.set_deserialize_strict({ { "layer_height", std::to_string(layer_height) },
|
||||
{ "initial_layer_print_height", std::to_string(layer_height) },
|
||||
{ "max_layer_height", max_layer_height },
|
||||
{ "wipe_tower_sparse_layers_combination", combine ? "1" : "0" } });
|
||||
return config;
|
||||
}
|
||||
|
||||
// What a sliced tower did with its sparse run.
|
||||
struct SparseRunResult { size_t planned, sparse, folded; float tallest_printed, printed_height; std::string gcode; };
|
||||
|
||||
static SparseRunResult slice_sparse_run(const DynamicPrintConfig &config)
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(10) }, print, model, config);
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
REQUIRE(print.is_step_done(psWipeTower));
|
||||
|
||||
SparseRunResult r{};
|
||||
for (const std::vector<WipeTower::ToolChangeResult> &layer : print.wipe_tower_data().tool_changes) {
|
||||
if (layer.empty())
|
||||
continue;
|
||||
++r.planned;
|
||||
if (wipe_tower_layer_is_sparse(layer))
|
||||
++r.sparse;
|
||||
if (wipe_tower_layer_is_combined_away(layer)) {
|
||||
++r.folded;
|
||||
} else {
|
||||
r.tallest_printed = std::max(r.tallest_printed, layer.front().layer_height);
|
||||
r.printed_height += layer.front().layer_height;
|
||||
}
|
||||
}
|
||||
r.gcode = Slic3r::Test::gcode(print);
|
||||
return r;
|
||||
}
|
||||
|
||||
// How often the G-code declares `height` in the tag this printer's processor reads. The dialect is a
|
||||
// global the exporter sets from the printer, so this is only correct after a slice - the point below.
|
||||
static size_t count_height_tags(const std::string &gcode, const char *height)
|
||||
{
|
||||
const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + height + "\n";
|
||||
size_t n = 0;
|
||||
for (size_t p = gcode.find(tag); p != std::string::npos; p = gcode.find(tag, p + 1))
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
TEST_CASE("Combining sparse layers folds a run into whole layers the nozzle can lay down", "[WipeTower]")
|
||||
{
|
||||
// 0.1 mm layers under a 0.32 mm cap: three fit (0.3), a fourth does not, so a run prints once
|
||||
// every three layers at 0.3 mm.
|
||||
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.1, "0.32", false));
|
||||
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.1, "0.32", true));
|
||||
|
||||
REQUIRE(plain.planned == combined.planned); // the plan still has one layer per object layer
|
||||
REQUIRE(plain.sparse > 10);
|
||||
CHECK(plain.folded == 0);
|
||||
CHECK_THAT(plain.tallest_printed, Catch::Matchers::WithinAbs(0.1f, 1e-4f));
|
||||
|
||||
CHECK(combined.folded > 0);
|
||||
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.3f, 1e-4f));
|
||||
// Two of every three sparse layers fold away, leaving the toolchange layers untouched.
|
||||
CHECK(combined.folded <= plain.sparse);
|
||||
CHECK(combined.folded >= plain.sparse / 2);
|
||||
// What folds away comes back as height on the layer that prints the run: no gap, nothing twice.
|
||||
CHECK_THAT(combined.printed_height, Catch::Matchers::WithinAbs(plain.printed_height, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("A run too thin to reach the nozzle's layer height is left alone", "[WipeTower]")
|
||||
{
|
||||
// Only whole layers merge, so two 0.2 mm layers (0.4) do not fit a 0.32 mm maximum and the tower
|
||||
// prints as if the option were off. This is the common 0.4 nozzle case; the tooltip says so.
|
||||
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.32", false));
|
||||
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.32", true));
|
||||
|
||||
REQUIRE(plain.sparse > 10);
|
||||
CHECK(combined.folded == 0);
|
||||
CHECK(combined.planned == plain.planned);
|
||||
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.2f, 1e-4f));
|
||||
}
|
||||
|
||||
TEST_CASE("A merged tower layer declares its own height to the G-code processor", "[WipeTower]")
|
||||
{
|
||||
// Each writer declares a height in a hardcoded tag dialect while the processor reads only its
|
||||
// printer's, so one of them is always dropped. A merged layer is the first time that shows, as a
|
||||
// thick layer drawn and costed as a thin one. 0.2 mm layers under a 0.42 mm maximum merge in pairs.
|
||||
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.42", false));
|
||||
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.42", true));
|
||||
|
||||
REQUIRE(combined.folded > 0);
|
||||
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.4f, 1e-4f));
|
||||
// Every layer that prints a merged run has to say so, and nothing may say so without the option.
|
||||
CHECK(count_height_tags(combined.gcode, "0.4") - count_height_tags(plain.gcode, "0.4") == combined.folded);
|
||||
}
|
||||
|
||||
TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]")
|
||||
{
|
||||
// Wrapping detection prints a tower on a plate that purges one filament. Neither the old
|
||||
|
||||
@@ -278,6 +278,98 @@ TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[Wip
|
||||
CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// "Combine sparse layers": folding a run of toolchange-free layers into one thicker tower layer.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
TEST_CASE("Sparse layers are combined only when every layer is still the tower's to place", "[WipeTower][CombineSparseLayers]") {
|
||||
PrintConfig cfg;
|
||||
cfg.timelapse_type.value = TimelapseType::tlTraditional;
|
||||
cfg.enable_wrapping_detection.value = false;
|
||||
cfg.wipe_tower_no_sparse_layers.value = false;
|
||||
|
||||
cfg.wipe_tower_sparse_layers_combination.value = false;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
|
||||
cfg.wipe_tower_sparse_layers_combination.value = true;
|
||||
CHECK(wipe_tower_sparse_layers_combined(cfg));
|
||||
|
||||
// Dropping the sparse layers outright leaves nothing to combine.
|
||||
cfg.wipe_tower_no_sparse_layers.value = true;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
|
||||
CHECK(wipe_tower_sparse_layers_skipped(cfg));
|
||||
cfg.wipe_tower_no_sparse_layers.value = false;
|
||||
|
||||
// Both of these park the nozzle on the tower every layer, so no layer may be folded away.
|
||||
cfg.timelapse_type.value = TimelapseType::tlSmooth;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
|
||||
cfg.timelapse_type.value = TimelapseType::tlTraditional;
|
||||
cfg.enable_wrapping_detection.value = true;
|
||||
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
|
||||
}
|
||||
|
||||
TEST_CASE("A layer folded into a later one is marked on the results the emitter reads", "[WipeTower][CombineSparseLayers]") {
|
||||
WipeTower::ToolChangeResult folded = make_tcr(1, 1, 0.2f);
|
||||
folded.combined_away = true;
|
||||
CHECK(wipe_tower_layer_is_combined_away({folded}));
|
||||
CHECK_FALSE(wipe_tower_layer_is_combined_away({make_tcr(1, 1, 0.2f)}));
|
||||
CHECK_FALSE(wipe_tower_layer_is_combined_away({}));
|
||||
}
|
||||
|
||||
TEST_CASE("A run of sparse layers prints once, on its last layer, at the height it covers", "[WipeTower][CombineSparseLayers]") {
|
||||
// Eight 0.1 mm layers on a 0.3 mm cap: a toolchange on the first and the last, sparse between.
|
||||
std::vector<float> heights(8, 0.1f);
|
||||
const std::vector<char> sparse{0, 1, 1, 1, 1, 1, 1, 0};
|
||||
const std::vector<float> caps(8, 0.3f);
|
||||
|
||||
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, sparse, caps, 0);
|
||||
REQUIRE(combined.size() == heights.size());
|
||||
// Three layers fill the cap exactly: the run flushes on layers 3 and 6, the two below each go.
|
||||
CHECK(combined == std::vector<char>{0, 1, 1, 0, 1, 1, 0, 0});
|
||||
CHECK_THAT(heights[3], WithinAbs(0.3f, 1e-5f));
|
||||
CHECK_THAT(heights[6], WithinAbs(0.3f, 1e-5f));
|
||||
// Layers that print keep the object covered: nothing is lost and nothing is printed twice.
|
||||
float printed = 0.f;
|
||||
for (size_t i = 0; i < heights.size(); ++i)
|
||||
if (! combined[i])
|
||||
printed += heights[i];
|
||||
CHECK_THAT(printed, WithinAbs(0.8f, 1e-5f));
|
||||
// A toolchange has to purge at its own z, so those layers are left exactly as planned.
|
||||
CHECK_THAT(heights[0], WithinAbs(0.1f, 1e-5f));
|
||||
CHECK_THAT(heights[7], WithinAbs(0.1f, 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("The maximum layer height of the nozzle that prints the run caps the merge", "[WipeTower][CombineSparseLayers]") {
|
||||
// The cap that counts belongs to the layer that prints the run; one that prints nothing lays
|
||||
// nothing down, so its own cap cannot constrain it. Five 0.1 mm layers, sparse above the first,
|
||||
// layer 3's nozzle taking only 0.15. (A real run holds one filament, so this only tests the
|
||||
// look-ahead.)
|
||||
std::vector<float> heights(5, 0.1f);
|
||||
std::vector<float> caps(5, 0.3f);
|
||||
caps[3] = 0.15f;
|
||||
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, {0, 1, 1, 1, 1}, caps, 0);
|
||||
// Layer 2 cannot hand its 0.2 mm on to layer 3, so it prints there and a fresh run starts above.
|
||||
CHECK(combined == std::vector<char>{0, 1, 0, 1, 0});
|
||||
CHECK_THAT(heights[2], WithinAbs(0.2f, 1e-5f));
|
||||
CHECK_THAT(heights[4], WithinAbs(0.2f, 1e-5f));
|
||||
|
||||
// A single layer already past the cap is printed as planned rather than shrunk.
|
||||
std::vector<float> tall{0.2f, 0.4f, 0.4f};
|
||||
const std::vector<char> tall_combined = combine_sparse_wipe_tower_layers(tall, {0, 1, 1}, {0.3f, 0.3f, 0.3f}, 0);
|
||||
CHECK(tall_combined == std::vector<char>{0, 0, 0});
|
||||
CHECK_THAT(tall[1], WithinAbs(0.4f, 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("The tower's first layer is never folded away", "[WipeTower][CombineSparseLayers]") {
|
||||
// It carries the brim and has to sit on the bed, however little it purges.
|
||||
std::vector<float> heights(4, 0.1f);
|
||||
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, {1, 1, 1, 1}, std::vector<float>(4, 0.5f), 0);
|
||||
CHECK(combined.front() == 0);
|
||||
CHECK_THAT(heights.front(), WithinAbs(0.1f, 1e-5f));
|
||||
// Everything above it merges into the top layer, which the cap still fits.
|
||||
CHECK(combined == std::vector<char>{0, 1, 1, 0});
|
||||
CHECK_THAT(heights.back(), WithinAbs(0.3f, 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") {
|
||||
// A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known,
|
||||
// so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.
|
||||
|
||||
Reference in New Issue
Block a user