mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-03 16:22:08 +00:00
225 lines
12 KiB
C++
225 lines
12 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include "libslic3r/GCodeReader.hpp"
|
|
|
|
#include "test_helpers.hpp"
|
|
|
|
#include <cctype>
|
|
#include <limits>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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
|
|
}
|
|
|
|
// With wait_for_temp_on_wipe_tower the blocking M109 moves from right after the Tn command to
|
|
// a stop point parked beside the wipe tower (heat-up drool falls next to the tower, not onto
|
|
// its top): tagged with _WAIT_FOR_TEMP_ON_WIPE_TOWER, after the toolchange and before the
|
|
// repositioning move and the first extrusion of the purge, while the post-toolchange restore
|
|
// demotes to a non-blocking M104. Ordering and the off-tower stop are the contract here.
|
|
TEST_CASE("Toolchange temperature wait moves to the wipe tower when enabled", "[MultiFilament]")
|
|
{
|
|
const bool wait_on_tower = GENERATE(false, true);
|
|
DYNAMIC_SECTION("wait_for_temp_on_wipe_tower " << (wait_on_tower ? 1 : 0)) {
|
|
const std::string gcode = slice_with_object_overrides(
|
|
{ cube(20), cube(20) },
|
|
multifilament_config(2, {
|
|
{ "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" },
|
|
{ "single_extruder_multi_material", 0 },
|
|
{ "enable_prime_tower", 1 },
|
|
{ "prime_tower_width", 35 },
|
|
{ "wipe_tower_x", "50" },
|
|
{ "wipe_tower_y", "50" },
|
|
{ "ooze_prevention", 1 },
|
|
{ "standby_temperature_delta", -40 },
|
|
{ "wait_for_temp_on_wipe_tower", wait_on_tower ? 1 : 0 },
|
|
}),
|
|
// One filament per object -> a toolchange on every layer. Assigned at the object
|
|
// level: the used-filament count that gates the prime tower is derived from
|
|
// object/volume configs on the harness's single apply (region filament ids such
|
|
// as sparse_infill_filament_id are not counted there and the tower would be
|
|
// silently disabled).
|
|
{ { { "extruder", 1 } }, { { "extruder", 2 } } });
|
|
|
|
// Split into lines and scan the "; CP TOOLCHANGE START".."; CP TOOLCHANGE END" blocks.
|
|
std::vector<std::string> lines;
|
|
for (size_t pos = 0; pos < gcode.size();) {
|
|
size_t eol = gcode.find('\n', pos);
|
|
if (eol == std::string::npos)
|
|
eol = gcode.size();
|
|
lines.emplace_back(gcode.substr(pos, eol - pos));
|
|
pos = eol + 1;
|
|
}
|
|
const auto is_tool_line = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char)l[1]); };
|
|
const auto is_m109_line = [](const std::string& l) { return l.rfind("M109", 0) == 0; };
|
|
const auto is_tagged_wait = [](const std::string& l) { return l.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") != std::string::npos; };
|
|
const auto is_extruding = [](const std::string& l) {
|
|
if (l.rfind("G1 ", 0) != 0)
|
|
return false;
|
|
const size_t e = l.find(" E");
|
|
return e != std::string::npos && l.find_first_of("XY") != std::string::npos && l[e + 2] != '-';
|
|
};
|
|
|
|
int checked_blocks = 0;
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
if (lines[i].find("; CP TOOLCHANGE START") == std::string::npos)
|
|
continue;
|
|
size_t block_end = i;
|
|
while (block_end < lines.size() && lines[block_end].find("; CP TOOLCHANGE END") == std::string::npos)
|
|
++block_end;
|
|
size_t tool_line = block_end;
|
|
for (size_t j = i; j < block_end; ++j)
|
|
if (is_tool_line(lines[j])) { tool_line = j; break; }
|
|
if (tool_line == block_end)
|
|
continue; // final unload block, no toolchange
|
|
++checked_blocks;
|
|
|
|
size_t tagged_wait = block_end, untagged_m109 = block_end, first_extrusion = block_end;
|
|
for (size_t j = tool_line + 1; j < block_end; ++j) {
|
|
if (is_m109_line(lines[j]) && tagged_wait == block_end && is_tagged_wait(lines[j]))
|
|
tagged_wait = j;
|
|
if (is_m109_line(lines[j]) && untagged_m109 == block_end && !is_tagged_wait(lines[j]))
|
|
untagged_m109 = j;
|
|
if (first_extrusion == block_end && is_extruding(lines[j]))
|
|
first_extrusion = j;
|
|
}
|
|
INFO("toolchange block at line " << i + 1);
|
|
if (wait_on_tower) {
|
|
// The only blocking wait is the tagged one, parked beside the tower before the purge.
|
|
REQUIRE(tagged_wait < block_end);
|
|
CHECK(untagged_m109 == block_end);
|
|
REQUIRE(first_extrusion < block_end);
|
|
CHECK(tagged_wait < first_extrusion);
|
|
// The travel preceding the wait parks outside the tower footprint. The tower
|
|
// auto-sizes, so derive its extent from the purge extrusions of this block.
|
|
size_t stop_line = block_end;
|
|
for (size_t j = tagged_wait; j-- > tool_line;)
|
|
if (lines[j].rfind("G1 ", 0) == 0 && lines[j].find('X') != std::string::npos) { stop_line = j; break; }
|
|
REQUIRE(stop_line < block_end);
|
|
const double stop_x = std::stod(lines[stop_line].substr(lines[stop_line].find('X') + 1));
|
|
double purge_min_x = std::numeric_limits<double>::max(), purge_max_x = std::numeric_limits<double>::lowest();
|
|
for (size_t j = tagged_wait; j < block_end; ++j) {
|
|
const size_t x_pos = lines[j].find('X');
|
|
if (!is_extruding(lines[j]) || x_pos == std::string::npos)
|
|
continue;
|
|
const double x = std::stod(lines[j].substr(x_pos + 1));
|
|
purge_min_x = std::min(purge_min_x, x);
|
|
purge_max_x = std::max(purge_max_x, x);
|
|
}
|
|
REQUIRE(purge_min_x <= purge_max_x);
|
|
INFO("stop travel: " << lines[stop_line] << " purge x range: " << purge_min_x << ".." << purge_max_x);
|
|
const bool beside_tower = stop_x < purge_min_x - 0.5 || stop_x > purge_max_x + 0.5;
|
|
CHECK(beside_tower);
|
|
} else {
|
|
// Stock behavior: the blocking wait follows the toolchange command directly.
|
|
REQUIRE(untagged_m109 < block_end);
|
|
CHECK(tagged_wait == block_end);
|
|
if (first_extrusion < block_end)
|
|
CHECK(untagged_m109 < first_extrusion);
|
|
}
|
|
i = block_end;
|
|
}
|
|
REQUIRE(checked_blocks > 0);
|
|
if (!wait_on_tower)
|
|
CHECK(gcode.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos);
|
|
}
|
|
}
|
|
|
|
// 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());
|
|
}
|