Files
OrcaSlicer/tests/fff_print/test_wipe.cpp
T
HanifKoh 4895bc03b4 Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced (#16099)
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced

Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes.

clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer.

* Remove Unused Project Includes From Files With Platform-Specific Code

A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block.

* Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass

The platform-file pass treated pchheader.hpp as an ordinary header and
emptied it, and left GUI_Preview.hpp and 14 other files relying on
headers they no longer reached directly.

* Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call

* Include Headers That Files Reached Through Ones the Cleanup Removed

* Drop Includes Duplicated by the Cleanup or by Main's Own Additions

* Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
2026-10-05 16:47:17 +08:00

671 lines
30 KiB
C++

#include <catch2/catch_all.hpp>
#include <algorithm>
#include <cmath>
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
#include <cstddef>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/calib.hpp"
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include <map>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Layer.hpp"
#include "test_helpers.hpp"
#include "libslic3r/Polyline.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, &region->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);
}