mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
654 lines
30 KiB
C++
654 lines
30 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <map>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
|
#include "libslic3r/GCodeReader.hpp"
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "libslic3r/Layer.hpp"
|
|
|
|
#include "test_helpers.hpp"
|
|
|
|
using namespace Slic3r;
|
|
using namespace Slic3r::Test;
|
|
|
|
namespace {
|
|
|
|
DynamicPrintConfig wipe_config(const char *wall_generator, bool wipe_inward,
|
|
const char *wipe_inward_distance = "50%",
|
|
const char *seam_gap = "10%", bool wipe_on_loops = false,
|
|
const char *wall_loops = "2",
|
|
const char *wall_sequence = "inner wall/outer wall",
|
|
bool alternate_extra_wall = false,
|
|
const char *sparse_infill_density = "0%",
|
|
const char *seam_position = "aligned")
|
|
{
|
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
|
config.set_deserialize_strict({
|
|
{ "nozzle_diameter", "0.4" },
|
|
{ "layer_height", "0.2" },
|
|
{ "initial_layer_print_height", "0.2" },
|
|
{ "line_width", "0.45" },
|
|
{ "outer_wall_line_width", "0" }, // Orca: Auto must use the actual path width.
|
|
{ "wall_loops", wall_loops },
|
|
{ "wall_generator", wall_generator },
|
|
{ "wall_sequence", wall_sequence },
|
|
{ "top_shell_layers", "0" },
|
|
{ "bottom_shell_layers", "0" },
|
|
{ "sparse_infill_density", sparse_infill_density },
|
|
{ "seam_position", seam_position },
|
|
{ "seam_gap", seam_gap },
|
|
{ "wipe", "1" },
|
|
{ "wipe_distance", "2" },
|
|
{ "retraction_length", "0.8" },
|
|
{ "retract_when_changing_layer", "1" },
|
|
{ "wipe_inward", wipe_inward ? "1" : "0" },
|
|
{ "wipe_inward_distance", wipe_inward_distance },
|
|
{ "wipe_on_loops", wipe_on_loops ? "1" : "0" },
|
|
{ "alternate_extra_wall", alternate_extra_wall ? "1" : "0" },
|
|
{ "gcode_comments", "1" },
|
|
{ "machine_start_gcode", "" },
|
|
{ "machine_end_gcode", "" },
|
|
});
|
|
return config;
|
|
}
|
|
|
|
struct WipeTrajectory {
|
|
Vec2d start;
|
|
double z;
|
|
std::vector<Vec2d> destinations;
|
|
};
|
|
|
|
std::vector<WipeTrajectory> wipe_trajectories(const std::string &gcode)
|
|
{
|
|
const std::string &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
|
const std::string &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
|
std::vector<WipeTrajectory> trajectories;
|
|
bool in_wipe = false;
|
|
|
|
GCodeReader parser;
|
|
parser.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
const std::string_view comment = line.comment();
|
|
if (comment.find(start_tag) != std::string_view::npos) {
|
|
in_wipe = true;
|
|
trajectories.push_back({Vec2d(self.x(), self.y()), self.z(), {}});
|
|
return;
|
|
}
|
|
if (comment.find(end_tag) != std::string_view::npos) {
|
|
in_wipe = false;
|
|
return;
|
|
}
|
|
if (in_wipe && line.dist_XY(self) > EPSILON)
|
|
trajectories.back().destinations.emplace_back(line.new_X(self), line.new_Y(self));
|
|
});
|
|
return trajectories;
|
|
}
|
|
|
|
std::vector<Vec2d> wipe_destinations(const std::string &gcode)
|
|
{
|
|
std::vector<Vec2d> destinations;
|
|
for (const WipeTrajectory &trajectory : wipe_trajectories(gcode))
|
|
destinations.insert(destinations.end(), trajectory.destinations.begin(), trajectory.destinations.end());
|
|
return destinations;
|
|
}
|
|
|
|
bool trajectories_differ(const std::vector<Vec2d> &lhs, const std::vector<Vec2d> &rhs)
|
|
{
|
|
if (lhs.size() != rhs.size())
|
|
return true;
|
|
for (size_t i = 0; i < lhs.size(); ++i)
|
|
if ((lhs[i] - rhs[i]).norm() > 0.01)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
double trajectory_length(const WipeTrajectory &trajectory)
|
|
{
|
|
double length = 0.;
|
|
Vec2d previous = trajectory.start;
|
|
for (const Vec2d &destination : trajectory.destinations) {
|
|
length += (destination - previous).norm();
|
|
previous = destination;
|
|
}
|
|
return length;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Wipe retraction preserves fractional speed with inward wipe disabled", "[Wipe][Regression]")
|
|
{
|
|
const char *retraction_speed = GENERATE("25.25", "25.5", "25.75");
|
|
const char *relative_e = GENERATE("0", "1");
|
|
INFO("retraction speed: " << retraction_speed);
|
|
INFO("relative E: " << relative_e);
|
|
DynamicPrintConfig config = wipe_config("classic", false);
|
|
config.set_deserialize_strict({
|
|
{"gcode_flavor", "marlin2"},
|
|
{"use_relative_e_distances", relative_e},
|
|
{"retraction_speed", retraction_speed},
|
|
{"retraction_length", "0.8"},
|
|
{"retract_before_wipe", "0%"},
|
|
{"retract_after_wipe", "0%"},
|
|
{"role_based_wipe_speed", "0"},
|
|
{"wipe_speed", "100"},
|
|
{"wipe_distance", "2"},
|
|
});
|
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
|
const auto &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
|
const auto &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
|
double before_wipe = 0.;
|
|
double during_wipe = 0.;
|
|
bool in_wipe = false;
|
|
bool complete = false;
|
|
GCodeReader parser;
|
|
parser.apply_config(config);
|
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (complete)
|
|
return;
|
|
if (line.comment().find(start_tag) != std::string_view::npos) {
|
|
in_wipe = true;
|
|
} else if (in_wipe && line.comment().find(end_tag) != std::string_view::npos) {
|
|
complete = true;
|
|
} else if (line.retracting(self)) {
|
|
(in_wipe ? during_wipe : before_wipe) -= line.dist_E(self);
|
|
} else if (line.extruding(self)) {
|
|
before_wipe = 0.;
|
|
}
|
|
});
|
|
|
|
REQUIRE(complete);
|
|
// At 100 mm/s, the 2 mm wipe lasts 0.02 seconds. The remaining part of
|
|
// the configured 0.8 mm retraction must be emitted before that wipe.
|
|
const double expected_during = std::stod(retraction_speed) * 2. / 100.;
|
|
CHECK_THAT(during_wipe, Catch::Matchers::WithinAbs(expected_during, 0.00005));
|
|
CHECK_THAT(before_wipe, Catch::Matchers::WithinAbs(0.8 - expected_during, 0.00005));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe respects the minimum travel for retraction and Z hop", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const char *relative_e = GENERATE("0", "1");
|
|
const char *reduce_crossing_wall = GENERATE("0", "1");
|
|
const char *minimum_travel = GENERATE("5", "0");
|
|
CAPTURE(wall_generator, relative_e, reduce_crossing_wall, minimum_travel);
|
|
DynamicPrintConfig config = wipe_config(
|
|
wall_generator, true, "50%", "10%", false, "3", "inner-outer-inner wall");
|
|
config.set_deserialize_strict({
|
|
{"gcode_flavor", "marlin2"},
|
|
{"use_relative_e_distances", relative_e},
|
|
{"reduce_crossing_wall", reduce_crossing_wall},
|
|
{"retraction_minimum_travel", minimum_travel},
|
|
{"retract_when_changing_layer", "0"},
|
|
{"use_firmware_retraction", "0"},
|
|
{"retract_before_wipe", "0%"},
|
|
{"retract_after_wipe", "0%"},
|
|
{"retraction_speed", "25.5"},
|
|
{"role_based_wipe_speed", "0"},
|
|
{"wipe_speed", "100"},
|
|
{"z_hop", "0.4"},
|
|
{"retract_lift_above", "0"},
|
|
{"retract_lift_below", "0"},
|
|
});
|
|
config.set_key_value("z_hop_types", new ConfigOptionEnumsGeneric{zhtNormal});
|
|
config.set_key_value("retract_lift_enforce", new ConfigOptionEnumsGeneric{rletAllSurfaces});
|
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
|
const auto &start_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
|
const auto &end_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_End);
|
|
ExtrusionRole role = erNone;
|
|
bool after_outer_wall = false;
|
|
bool in_wipe = false;
|
|
size_t transitions = 0;
|
|
size_t same_layer_transitions = 0;
|
|
size_t inward_wipes = 0;
|
|
double retraction = 0.;
|
|
double lift = 0.;
|
|
double outer_z = 0.;
|
|
GCodeReader parser;
|
|
parser.apply_config(config);
|
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (line.comment().find(role_tag) == 0)
|
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
|
if (line.comment().find(start_tag) == 0) {
|
|
in_wipe = true;
|
|
if (after_outer_wall)
|
|
++inward_wipes;
|
|
} else if (line.comment().find(end_tag) == 0) {
|
|
in_wipe = false;
|
|
}
|
|
if (line.extruding(self) && line.dist_XY(self) > EPSILON) {
|
|
if (role == erExternalPerimeter) {
|
|
after_outer_wall = true;
|
|
retraction = lift = 0.;
|
|
outer_z = line.new_Z(self);
|
|
} else if (after_outer_wall) {
|
|
REQUIRE(role == erPerimeter);
|
|
++transitions;
|
|
const double layer_rise = std::max(0., double(self.z()) - outer_z);
|
|
if (layer_rise < EPSILON)
|
|
++same_layer_transitions;
|
|
// A 5 mm threshold suppresses retraction across a few wall widths.
|
|
// A zero threshold still permits the ordinary retract and lift.
|
|
const bool retract = std::stod(minimum_travel) == 0.;
|
|
CHECK_THAT(retraction, Catch::Matchers::WithinAbs(retract ? 0.8 : 0., 0.00005));
|
|
// Exclude an ordinary layer change from the accumulated upward motion.
|
|
CHECK_THAT(lift - layer_rise, Catch::Matchers::WithinAbs(retract ? 0.4 : 0., 0.001));
|
|
after_outer_wall = false;
|
|
}
|
|
} else if (after_outer_wall) {
|
|
if (line.retracting(self))
|
|
retraction -= line.dist_E(self);
|
|
lift += std::max(0., double(line.dist_Z(self)));
|
|
if (in_wipe)
|
|
CHECK_THAT(line.dist_E(self), Catch::Matchers::WithinAbs(0., 0.00005));
|
|
}
|
|
});
|
|
// The 1 mm cube has five 0.2 mm layers: every outer wall must still wipe.
|
|
REQUIRE(transitions == 5);
|
|
REQUIRE(same_layer_transitions >= 4);
|
|
REQUIRE(inward_wipes == transitions);
|
|
}
|
|
|
|
TEST_CASE("Changing inward wipe settings preserves the sliced geometry", "[Wipe][Regression]")
|
|
{
|
|
const char *key = GENERATE("wipe_inward", "wipe_inward_distance");
|
|
DynamicPrintConfig config = wipe_config("classic", false);
|
|
Print print;
|
|
Model model;
|
|
init_print({make_cube(10., 10., 1.)}, print, model, config);
|
|
gcode(print);
|
|
const PrintObject &object = *print.objects().front();
|
|
REQUIRE(object.is_step_done(posPerimeters));
|
|
REQUIRE(object.is_step_done(posInfill));
|
|
REQUIRE(print.is_step_done(psWipeTower));
|
|
REQUIRE(print.is_step_done(psGCodeExport));
|
|
|
|
DynamicPrintConfig changed = config;
|
|
changed.set_deserialize_strict({{key, std::string(key) == "wipe_inward" ? "1" : "75%"}});
|
|
print.apply(model, changed);
|
|
|
|
CHECK(print.objects().front()->is_step_done(posPerimeters));
|
|
CHECK(print.objects().front()->is_step_done(posInfill));
|
|
CHECK(print.is_step_done(psWipeTower));
|
|
CHECK_FALSE(print.is_step_done(psGCodeExport));
|
|
}
|
|
|
|
TEST_CASE("Retraction and pressure advance calibration suppress inward wipe overrides", "[Wipe][Regression]")
|
|
{
|
|
const auto mode = GENERATE(CalibMode::Calib_None, CalibMode::Calib_PA_Tower,
|
|
CalibMode::Calib_Auto_PA_Line, CalibMode::Calib_Retraction_tower,
|
|
CalibMode::Calib_Flow_Rate);
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const bool per_object = GENERATE(false, true);
|
|
INFO("calibration mode: " << int(mode) << ", wall generator: " << wall_generator
|
|
<< ", per-object override: " << per_object);
|
|
|
|
const auto trajectories = [&](bool inward) {
|
|
DynamicPrintConfig config = wipe_config(wall_generator, inward && !per_object);
|
|
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{
|
|
{{"wipe_inward", inward ? "1" : "0"}}
|
|
};
|
|
Print print;
|
|
Model model;
|
|
init_print({make_cube(10., 10., 1.)}, print, model, config, per_object ? &overrides : nullptr);
|
|
Calib_Params params;
|
|
params.mode = mode;
|
|
params.start = 0.2;
|
|
params.end = 0.4;
|
|
params.step = 0.1;
|
|
print.set_calib_params(params);
|
|
return wipe_destinations(gcode(print));
|
|
};
|
|
|
|
const auto regular = trajectories(false);
|
|
const auto inward = trajectories(true);
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(inward.empty());
|
|
// Other calibration modes and ordinary prints must still honor the option.
|
|
const bool should_differ = mode == CalibMode::Calib_None || mode == CalibMode::Calib_Flow_Rate;
|
|
CHECK(trajectories_differ(regular, inward) == should_differ);
|
|
}
|
|
|
|
TEST_CASE("Inactive inward wipe settings preserve the exported trajectory", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const bool disable_wiping = GENERATE(false, true);
|
|
DynamicPrintConfig regular = wipe_config(wall_generator, false);
|
|
DynamicPrintConfig inward = wipe_config(wall_generator, true, disable_wiping ? "50%" : "0");
|
|
if (disable_wiping) {
|
|
regular.set_deserialize_strict({{"wipe", "0"}});
|
|
inward.set_deserialize_strict({{"wipe", "0"}});
|
|
}
|
|
const auto regular_paths = wipe_destinations(slice({make_cube(10., 10., 1.)}, regular));
|
|
const auto inward_paths = wipe_destinations(slice({make_cube(10., 10., 1.)}, inward));
|
|
if (!disable_wiping)
|
|
REQUIRE_FALSE(regular_paths.empty());
|
|
CHECK_FALSE(trajectories_differ(regular_paths, inward_paths));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe changes the exported trajectory when outer wall width is Auto", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false)));
|
|
const std::vector<Vec2d> inward = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true)));
|
|
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(inward.empty());
|
|
REQUIRE(trajectories_differ(regular, inward));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe recognizes an external wall starting on an overhang", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const bool inward = GENERATE(false, true);
|
|
CAPTURE(wall_generator, inward);
|
|
const auto config = wipe_config(wall_generator, inward, "50%", "0%", false,
|
|
"3", "inner-outer-inner wall", false, "0%", "back");
|
|
Print print;
|
|
Model model;
|
|
init_print({make_cube(10., 10., 1.)}, print, model, config);
|
|
print.process();
|
|
size_t mixed_loops = 0;
|
|
const auto mark_overhangs = [&](auto &&self, ExtrusionEntity *entity) -> void {
|
|
if (auto *collection = dynamic_cast<ExtrusionEntityCollection *>(entity)) {
|
|
for (ExtrusionEntity *child : collection->entities)
|
|
self(self, child);
|
|
} else if (auto *loop = dynamic_cast<ExtrusionLoop *>(entity); loop && is_external_perimeter(loop->role())) {
|
|
// Keep the printed geometry intact and give the back seam overhang
|
|
// roles. The front edge remains an ordinary external-wall segment.
|
|
ExtrusionPaths paths;
|
|
bool has_overhang = false;
|
|
bool has_external = false;
|
|
for (const ExtrusionPath &source : loop->paths) {
|
|
for (size_t i = 1; i < source.polyline.points.size(); ++i) {
|
|
ExtrusionPath path = source;
|
|
path.polyline.points = {source.polyline.points[i - 1], source.polyline.points[i]};
|
|
const bool overhang = path.polyline.points.front().y() > 0 || path.polyline.points.back().y() > 0;
|
|
path.set_extrusion_role(overhang ? erOverhangPerimeter : erExternalPerimeter);
|
|
has_overhang |= overhang;
|
|
has_external |= !overhang;
|
|
paths.push_back(std::move(path));
|
|
}
|
|
}
|
|
REQUIRE(has_overhang);
|
|
REQUIRE(has_external);
|
|
loop->paths = std::move(paths);
|
|
++mixed_loops;
|
|
}
|
|
};
|
|
for (const PrintObject *object : print.objects())
|
|
for (Layer *layer : object->layers())
|
|
for (LayerRegion *region : layer->regions())
|
|
mark_overhangs(mark_overhangs, ®ion->perimeters);
|
|
REQUIRE(mixed_loops > 0);
|
|
|
|
bool has_inward_wipe = false;
|
|
for (const WipeTrajectory &trajectory : wipe_trajectories(gcode(print))) {
|
|
if (trajectory.destinations.empty())
|
|
continue;
|
|
const Vec2d move = trajectory.destinations.front() - trajectory.start;
|
|
if (trajectory.start.x() > 4. && trajectory.start.y() > 4. && move.x() < -0.05 && move.y() < -0.05)
|
|
has_inward_wipe = true;
|
|
}
|
|
CHECK(has_inward_wipe == inward);
|
|
}
|
|
|
|
TEST_CASE("Inward wipe keeps its offset when seam gap is zero", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false, "50%", "0%")));
|
|
const std::vector<Vec2d> inward = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "50%", "0%")));
|
|
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(inward.empty());
|
|
REQUIRE(trajectories_differ(regular, inward));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe is retained across layers with a back seam", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const DynamicPrintConfig inward_config = wipe_config(
|
|
wall_generator, true, "50%", "0%", false, "3", "inner-outer-inner wall", false, "0%", "back");
|
|
const std::vector<WipeTrajectory> inward = wipe_trajectories(slice({make_cube(27., 27., 1.)}, inward_config));
|
|
|
|
REQUIRE_FALSE(inward.empty());
|
|
std::map<double, bool> inward_wipe_by_layer;
|
|
for (const WipeTrajectory &trajectory : inward) {
|
|
bool &has_inward_wipe = inward_wipe_by_layer[trajectory.z];
|
|
if (trajectory.destinations.empty())
|
|
continue;
|
|
const Vec2d first_move = trajectory.destinations.front() - trajectory.start;
|
|
// Orca: a back seam lands on the cube's positive-X/positive-Y corner.
|
|
// Its inward wipe must move diagonally away from both external faces.
|
|
has_inward_wipe = has_inward_wipe ||
|
|
(trajectory.start.x() > 13. && trajectory.start.y() > 13. &&
|
|
first_move.x() < -0.05 && first_move.y() < -0.05);
|
|
}
|
|
REQUIRE(inward_wipe_by_layer.size() == 5);
|
|
for (const auto &[z, has_inward_wipe] : inward_wipe_by_layer) {
|
|
INFO("layer Z: " << z);
|
|
REQUIRE(has_inward_wipe);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Literal inward wipe distance is clamped to the outer wall width", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false)));
|
|
const std::vector<Vec2d> full_width = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "100%")));
|
|
const std::vector<Vec2d> oversized = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "2")));
|
|
|
|
REQUIRE_FALSE(full_width.empty());
|
|
REQUIRE(trajectories_differ(regular, full_width));
|
|
REQUIRE(oversized.size() == full_width.size());
|
|
for (size_t i = 0; i < full_width.size(); ++i)
|
|
REQUIRE_THAT((oversized[i] - full_width[i]).norm(), Catch::Matchers::WithinAbs(0., 0.01));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe is not applied without an adjacent wall", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, false, "50%", "10%", false, "1")));
|
|
const std::vector<Vec2d> inward = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(wall_generator, true, "50%", "10%", false, "1")));
|
|
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(trajectories_differ(regular, inward));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe uses an alternate extra wall when the configured wall count is one", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const DynamicPrintConfig regular_config = wipe_config(
|
|
wall_generator, false, "50%", "10%", false, "1", "inner wall/outer wall", true, "15%");
|
|
const DynamicPrintConfig inward_config = wipe_config(
|
|
wall_generator, true, "50%", "10%", false, "1", "inner wall/outer wall", true, "15%");
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, regular_config));
|
|
const std::vector<Vec2d> inward = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, inward_config));
|
|
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(inward.empty());
|
|
REQUIRE(trajectories_differ(regular, inward));
|
|
}
|
|
|
|
TEST_CASE("Inward wipe is not applied before the adjacent wall is printed", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
const std::vector<Vec2d> regular = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(
|
|
wall_generator, false, "50%", "10%", false, "2", "outer wall/inner wall")));
|
|
const std::vector<Vec2d> inward = wipe_destinations(
|
|
slice({make_cube(10., 10., 1.)}, wipe_config(
|
|
wall_generator, true, "50%", "10%", false, "2", "outer wall/inner wall")));
|
|
|
|
REQUIRE_FALSE(regular.empty());
|
|
REQUIRE_FALSE(trajectories_differ(regular, inward));
|
|
}
|
|
|
|
TEST_CASE("Wipe on loops preserves the corner move with inward wipe disabled", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const char *nozzle_diameter = GENERATE("0.4", "0.8");
|
|
const char *comments = GENERATE("0", "1");
|
|
CAPTURE(comments);
|
|
INFO("wall generator: " << wall_generator << ", nozzle diameter: " << nozzle_diameter);
|
|
// A closed square gives a 90-degree material-side corner at the seam.
|
|
DynamicPrintConfig config = wipe_config(wall_generator, false, "50%", "0", true);
|
|
config.set_deserialize_strict({{"nozzle_diameter", nozzle_diameter}, {"seam_position", "nearest"},
|
|
{"gcode_comments", comments}});
|
|
const std::string output = slice({make_cube(10., 10., 1.)}, config);
|
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
|
const auto &wipe_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
|
ExtrusionRole role = erNone;
|
|
std::vector<Vec2d> loop;
|
|
bool after_extrusion = false;
|
|
size_t moves = 0;
|
|
GCodeReader parser;
|
|
parser.apply_config(config);
|
|
parser.parse_buffer(output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (line.comment().find(role_tag) == 0) {
|
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
|
loop.clear();
|
|
after_extrusion = false;
|
|
}
|
|
if (line.comment().find(wipe_tag) == 0)
|
|
after_extrusion = false;
|
|
if (role != erExternalPerimeter)
|
|
return;
|
|
if (line.extruding(self) && line.dist_XY(self) > EPSILON) {
|
|
if (loop.empty())
|
|
loop.emplace_back(self.x(), self.y());
|
|
loop.emplace_back(line.new_X(self), line.new_Y(self));
|
|
after_extrusion = true;
|
|
return;
|
|
}
|
|
// The loop move is the first non-extruding XY move after the external
|
|
// wall and before the reserved wipe marker, regardless of comment text.
|
|
if (!after_extrusion || line.dist_XY(self) <= EPSILON)
|
|
return;
|
|
after_extrusion = false;
|
|
|
|
++moves;
|
|
INFO("layer Z: " << self.z());
|
|
REQUIRE(loop.size() >= 4);
|
|
const Vec2d seam = loop.front();
|
|
REQUIRE_THAT((loop.back() - seam).norm(), Catch::Matchers::WithinAbs(0., 0.003));
|
|
const Vec2d outgoing = (loop[1] - seam).normalized();
|
|
const Vec2d into_corner = (loop[loop.size() - 2] - seam).normalized();
|
|
REQUIRE_THAT(outgoing.dot(into_corner), Catch::Matchers::WithinAbs(0., 0.01));
|
|
const Vec2d move = Vec2d(line.new_X(self), line.new_Y(self)) - seam;
|
|
// The legacy corner move is 20% of the nozzle diameter, turned 30 degrees
|
|
// from the outgoing edge into the square. Check both components independently.
|
|
const double distance = 0.2 * std::stod(nozzle_diameter);
|
|
CHECK_THAT(move.dot(outgoing), Catch::Matchers::WithinAbs(distance * std::sqrt(3.) / 2., 0.003));
|
|
CHECK_THAT(move.dot(into_corner), Catch::Matchers::WithinAbs(distance / 2., 0.003));
|
|
});
|
|
REQUIRE(moves == 5);
|
|
}
|
|
|
|
TEST_CASE("Inward wipe remains valid after wipe on loops moves the nozzle", "[Wipe][Regression]")
|
|
{
|
|
const char *wall_generator = GENERATE("classic", "arachne");
|
|
const char *comments = GENERATE("0", "1");
|
|
CAPTURE(comments);
|
|
INFO("wall generator: " << wall_generator);
|
|
|
|
DynamicPrintConfig config = wipe_config(wall_generator, false, "50%", "10%", true);
|
|
config.set_deserialize_strict({{"gcode_comments", comments}});
|
|
const std::string loop_move = slice({make_cube(10., 10., 1.)}, config);
|
|
config.set_deserialize_strict({{"wipe_inward", "1"}});
|
|
const std::string combined = slice({make_cube(10., 10., 1.)}, config);
|
|
config.set_deserialize_strict({{"wipe_on_loops", "0"}});
|
|
const std::string inward_only = slice({make_cube(10., 10., 1.)}, config);
|
|
|
|
for (const std::string *output : {&loop_move, &combined}) {
|
|
INFO("wipe_inward: " << (output == &combined));
|
|
std::map<double, std::vector<Vec2d>> loop_moves_by_layer;
|
|
const auto &role_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role);
|
|
const auto &wipe_tag = GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Start);
|
|
ExtrusionRole role = erNone;
|
|
bool after_extrusion = false;
|
|
GCodeReader parser;
|
|
parser.apply_config(config);
|
|
parser.parse_buffer(*output, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
|
if (line.comment().find(role_tag) == 0) {
|
|
role = ExtrusionEntity::string_to_role(line.comment().substr(role_tag.size()));
|
|
after_extrusion = false;
|
|
}
|
|
if (line.comment().find(wipe_tag) == 0)
|
|
after_extrusion = false;
|
|
if (role != erExternalPerimeter || line.dist_XY(self) <= EPSILON)
|
|
return;
|
|
if (line.extruding(self)) {
|
|
after_extrusion = true;
|
|
} else if (after_extrusion) {
|
|
loop_moves_by_layer[line.new_Z(self)].emplace_back(line.new_X(self), line.new_Y(self));
|
|
after_extrusion = false;
|
|
}
|
|
});
|
|
|
|
// The 1 mm cube at 0.2 mm layer height has one external loop on each of five layers.
|
|
const auto trajectories = wipe_trajectories(*output);
|
|
REQUIRE(loop_moves_by_layer.size() == 5);
|
|
for (size_t layer = 1; layer <= 5; ++layer) {
|
|
const double z = layer * 0.2;
|
|
const auto moves = std::find_if(loop_moves_by_layer.begin(), loop_moves_by_layer.end(),
|
|
[z](const auto &entry) { return std::abs(entry.first - z) < 0.001; });
|
|
REQUIRE(moves != loop_moves_by_layer.end());
|
|
REQUIRE(moves->second.size() == 1);
|
|
const auto wipe = std::find_if(trajectories.begin(), trajectories.end(), [&](const WipeTrajectory &trajectory) {
|
|
return std::abs(trajectory.z - z) < 0.001 &&
|
|
(trajectory.start - moves->second.front()).norm() < 0.001;
|
|
});
|
|
REQUIRE(wipe != trajectories.end());
|
|
// The configured 2 mm wipe must be measured from the inward move's
|
|
// endpoint, including when wipe_inward is off (set_last_pos regression).
|
|
CHECK_THAT(trajectory_length(*wipe), Catch::Matchers::WithinAbs(2., 0.003));
|
|
}
|
|
}
|
|
|
|
const std::vector<WipeTrajectory> combined_trajectories = wipe_trajectories(combined);
|
|
const std::vector<WipeTrajectory> inward_trajectories = wipe_trajectories(inward_only);
|
|
REQUIRE_FALSE(combined_trajectories.empty());
|
|
REQUIRE(combined_trajectories.size() == inward_trajectories.size());
|
|
REQUIRE(trajectories_differ(wipe_destinations(combined), wipe_destinations(loop_move)));
|
|
|
|
bool start_changed = false;
|
|
for (size_t i = 0; i < combined_trajectories.size(); ++i) {
|
|
start_changed = start_changed ||
|
|
(combined_trajectories[i].start - inward_trajectories[i].start).norm() > 0.01;
|
|
REQUIRE_THAT(trajectory_length(combined_trajectories[i]),
|
|
Catch::Matchers::WithinAbs(trajectory_length(inward_trajectories[i]), 0.01));
|
|
}
|
|
REQUIRE(start_changed);
|
|
}
|