mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-01 14:27:00 +00:00
Merge branch 'main' into feature/filament_id
This commit is contained in:
@@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_helpers.hpp
|
||||
test_cooling.cpp
|
||||
test_extrusion_entity.cpp
|
||||
test_extrusion_processor.cpp
|
||||
test_fill.cpp
|
||||
test_flow.cpp
|
||||
test_gcode_timing.cpp
|
||||
|
||||
441
tests/fff_print/test_extrusion_processor.cpp
Normal file
441
tests/fff_print/test_extrusion_processor.cpp
Normal file
@@ -0,0 +1,441 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/AABBTreeLines.hpp"
|
||||
#include "libslic3r/GCode/ExtrusionProcessor.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
|
||||
namespace {
|
||||
|
||||
// Print settings the assertions below are derived from.
|
||||
constexpr double caged_layer_height = 0.2; // mm
|
||||
constexpr double caged_wall_width = 0.42; // mm, outer wall line width
|
||||
constexpr double caged_outer_wall_speed = 200.; // mm/s
|
||||
constexpr double caged_slow_speed = 100.; // mm/s, between every configured overhang speed (<= 50) and the wall speed
|
||||
|
||||
// A wall running 0.2mm out over a previous layer whose edge dishes 0.03mm away from it in the middle,
|
||||
// standing in for the endpoint readings a caged overhang perimeter takes: enough of a difference to
|
||||
// print at another speed, but only a fraction of the distance at which slowdown begins.
|
||||
constexpr double dished_wall_gap = 0.2; // mm, how far the wall runs out past the previous layer's edge
|
||||
constexpr double dished_layer_depth = 0.03; // mm, how much further out the middle of it reads
|
||||
constexpr double dished_min_distance = 0.042; // mm, the reading at which the configured speeds begin to slow down
|
||||
// Every reading here is past that, so the whole wall is slowed and only the amount is in question.
|
||||
constexpr float dished_end_reading = float(dished_wall_gap + 0.5 * caged_wall_width);
|
||||
constexpr float dished_mid_reading = float(dished_end_reading + dished_layer_depth);
|
||||
// The two readings are dished_layer_depth apart, so half of that tells them apart while still allowing
|
||||
// for the points the passes after sampling add, which read a little further out than the ends do.
|
||||
constexpr double dished_reading_tolerance = 0.5 * dished_layer_depth;
|
||||
|
||||
// A 40 x 20 x 20 mm box with a 45 degree overhang cut into the y = 0 side. The sloped face spans
|
||||
// x = 5.086 .. 34.914 only, so the full-height walls of the box cage both ends of every overhang
|
||||
// perimeter: the endpoints look supported even though the span between them is not.
|
||||
TriangleMesh caged_overhang_mesh()
|
||||
{
|
||||
return TriangleMesh(
|
||||
{
|
||||
{5.0859987f, 10.167065f, 5.711731f}, {34.914257f, 10.167065f, 5.711731f},
|
||||
{34.914257f, 0.f, 15.878796f}, {5.0859995f, 0.f, 15.878796f},
|
||||
{0.f, 0.f, 0.f}, {0.f, 0.f, 20.f},
|
||||
{0.f, 20.f, 20.f}, {0.f, 20.f, 0.f},
|
||||
{40.f, 20.f, 20.f}, {40.f, 20.f, 0.f},
|
||||
{40.f, 0.f, 20.f}, {40.f, 0.f, 0.f},
|
||||
{34.914257f, 0.f, 0.f}, {5.0859995f, 0.f, 0.f},
|
||||
{34.914257f, 10.167065f, 0.f}, {5.0859995f, 10.167065f, 0.f},
|
||||
},
|
||||
{
|
||||
{0, 1, 2}, {0, 2, 3}, {4, 5, 6}, {4, 6, 7}, {7, 6, 8}, {7, 8, 9},
|
||||
{9, 8, 10}, {9, 10, 11}, {12, 11, 10}, {5, 4, 13}, {5, 13, 3}, {2, 12, 10},
|
||||
{5, 3, 2}, {10, 5, 2}, {9, 11, 12}, {9, 12, 14}, {13, 4, 7}, {9, 14, 15},
|
||||
{15, 13, 7}, {7, 9, 15}, {8, 6, 5}, {8, 5, 10}, {14, 1, 0}, {14, 0, 15},
|
||||
{2, 1, 14}, {2, 14, 12}, {15, 0, 3}, {15, 3, 13},
|
||||
});
|
||||
}
|
||||
|
||||
// Mesh geometry the wall filters below are derived from.
|
||||
constexpr double caged_box_depth = 20.; // mm, the box spans y = 0 .. 20
|
||||
constexpr double caged_slope_face_sum = 15.878796; // mm, y + z of the sloped face, from its corners
|
||||
// The sloped face spans this x range; outside it the box walls run full height.
|
||||
constexpr double caged_slope_x_min = 5.0859995;
|
||||
constexpr double caged_slope_x_max = 34.914257;
|
||||
constexpr double caged_slope_span = caged_slope_x_max - caged_slope_x_min; // ~29.8 mm
|
||||
// The z range the sloped face occupies, from the same fixture vertices.
|
||||
constexpr double caged_slope_z_min = 5.711731;
|
||||
constexpr double caged_slope_z_max = 15.878796;
|
||||
// The lowest slope layer still sits on the solid body below the notch, so it is fully supported and
|
||||
// runs at the outer wall speed by design. The caged span proper begins one layer above it.
|
||||
constexpr double caged_span_z_min = caged_slope_z_min + caged_layer_height;
|
||||
|
||||
// A layer printed at z is sliced at z - layer_height / 2, and the outer wall centreline sits half a
|
||||
// line width inside the contour, so the wall on the slope satisfies y + z = 16.189.
|
||||
constexpr double caged_slope_wall_sum = caged_slope_face_sum + 0.5 * caged_layer_height + 0.5 * caged_wall_width;
|
||||
// Same inset on the fully supported y = 20 face, vertical over the whole height.
|
||||
constexpr double caged_back_wall_y = caged_box_depth - 0.5 * caged_wall_width;
|
||||
// And on the y = 0 face, which runs full height only outside the slope's x range.
|
||||
constexpr double caged_front_wall_y = 0.5 * caged_wall_width;
|
||||
// Arachne varies the wall width along a face, and the centreline inset is half that width, so a
|
||||
// wall sits within about half a line width of where the nominal inset alone would put it. The
|
||||
// faces being selected are millimetres apart, so this stays far from ambiguous.
|
||||
constexpr double caged_wall_tolerance = 0.5 * caged_wall_width;
|
||||
|
||||
// Feed rates in mm/min of the long outer wall extrusions `keep_line` selects.
|
||||
template<typename KeepLine> std::vector<double> outer_wall_feed_rates(const std::string& gcode, KeepLine keep_line)
|
||||
{
|
||||
std::vector<double> feed_rates;
|
||||
bool outer_wall = false;
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&feed_rates, &outer_wall, &keep_line](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)
|
||||
outer_wall = comment.find("Outer wall") != std::string_view::npos ||
|
||||
comment.find("External perimeter") != std::string_view::npos;
|
||||
|
||||
if (outer_wall && line.extruding(self) && line.dist_XY(self) > 1.0 && keep_line(self, line))
|
||||
feed_rates.push_back(line.new_F(self));
|
||||
});
|
||||
|
||||
return feed_rates;
|
||||
}
|
||||
|
||||
// The caged 45 degree overhang: outer walls crossing the sloped face for most of its width, on the
|
||||
// layers where the face genuinely overhangs.
|
||||
// Both ends are tested against the slope plane rather than requiring a constant Y. Arachne's
|
||||
// variable-width walls drift slightly in Y along the same slope (Y6.186 -> Y6.189 on one move), so
|
||||
// a constant-Y filter matches almost nothing under Arachne and silently reduces its coverage.
|
||||
// The length test excludes the cage walls: they are only as wide as the box is either side of the
|
||||
// slope, but being vertical their y + z sweeps through the slope plane as z rises, so a couple of
|
||||
// their fully supported moves would otherwise be counted as part of the span.
|
||||
std::vector<double> caged_slope_feed_rates(const std::string& gcode)
|
||||
{
|
||||
return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
const double z = line.new_Z(self);
|
||||
return z > caged_span_z_min && z < caged_slope_z_max &&
|
||||
line.dist_XY(self) > 0.5 * caged_slope_span &&
|
||||
std::abs(self.y() + z - caged_slope_wall_sum) < caged_wall_tolerance &&
|
||||
std::abs(line.new_Y(self) + z - caged_slope_wall_sum) < caged_wall_tolerance;
|
||||
});
|
||||
}
|
||||
|
||||
// The opposite, fully supported face, skipping the initial layer and its own speed settings.
|
||||
std::vector<double> back_wall_feed_rates(const std::string& gcode)
|
||||
{
|
||||
return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
return line.new_Z(self) > 1.5 * caged_layer_height &&
|
||||
std::abs(self.y() - caged_back_wall_y) < caged_wall_tolerance &&
|
||||
std::abs(line.new_Y(self) - caged_back_wall_y) < caged_wall_tolerance;
|
||||
});
|
||||
}
|
||||
|
||||
// The first layer printed entirely above the slope. Its y = 0 wall runs the full width of the box.
|
||||
const double caged_layer_above_slope_z = std::ceil(caged_slope_z_max / caged_layer_height) * caged_layer_height;
|
||||
|
||||
// The parts of that wall standing on the cage rather than the slope, so on a contour identical to their own.
|
||||
// Where the support changes is found by bisection, which stops at spans of 2mm, so the move spanning each end of
|
||||
// the slope reaches a little way into the cage. Taking only the moves lying wholly outside the slope's x range
|
||||
// leaves the wall that is unambiguously supported, without asserting how closely the bisection converged.
|
||||
std::vector<double> cage_shoulder_feed_rates(const std::string& gcode)
|
||||
{
|
||||
return outer_wall_feed_rates(gcode, [](const GCodeReader& self, const GCodeReader::GCodeLine& line) {
|
||||
return std::abs(line.new_Z(self) - caged_layer_above_slope_z) < 0.5 * caged_layer_height &&
|
||||
std::abs(self.y() - caged_front_wall_y) < caged_wall_tolerance &&
|
||||
std::abs(line.new_Y(self) - caged_front_wall_y) < caged_wall_tolerance &&
|
||||
(std::max(self.x(), line.new_X(self)) <= caged_slope_x_min ||
|
||||
std::min(self.x(), line.new_X(self)) >= caged_slope_x_max);
|
||||
});
|
||||
}
|
||||
|
||||
// The readings a 40mm wall takes over a previous layer whose edge falls away by 0.03mm towards the
|
||||
// middle: both ends read the same, and the middle reads slightly further out over air. Whether that
|
||||
// middle reading survives is what decides the speed the wall is printed at.
|
||||
std::vector<ExtendedPoint<2>> sampled_wall_over_dished_layer(const std::function<float(float)>& distance_to_speed)
|
||||
{
|
||||
const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{
|
||||
{{0., 0.}, {20., -dished_layer_depth}},
|
||||
{{20., -dished_layer_depth}, {40., 0.}},
|
||||
{{40., 0.}, {40., -10.}},
|
||||
{{40., -10.}, {0., -10.}},
|
||||
{{0., -10.}, {0., 0.}},
|
||||
});
|
||||
const Points wall{Point::new_scale(0., dished_wall_gap), Point::new_scale(40., dished_wall_gap)};
|
||||
|
||||
return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f,
|
||||
dished_min_distance, distance_to_speed);
|
||||
}
|
||||
|
||||
// A straight, otherwise supported wall over a previous-layer boundary with a 2mm-wide pocket. Moving the
|
||||
// pocket between x = 10 and x = 20 covers both discovery away from the wall's midpoint and refinement around
|
||||
// a midpoint that has already been discovered. The current wall is inset half its width from the flat boundary,
|
||||
// so its supported readings are zero after the estimator applies its boundary offset.
|
||||
constexpr double narrow_pocket_wall_length = 40.;
|
||||
constexpr double narrow_pocket_width = 2.;
|
||||
constexpr double narrow_pocket_depth = 0.3;
|
||||
|
||||
std::vector<ExtendedPoint<2>> sampled_wall_over_narrow_pocket(
|
||||
double pocket_center, const std::function<float(float)>& distance_to_speed)
|
||||
{
|
||||
const double pocket_left = pocket_center - 0.5 * narrow_pocket_width;
|
||||
const double pocket_right = pocket_center + 0.5 * narrow_pocket_width;
|
||||
const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{
|
||||
{{0., 0.}, {pocket_left, 0.}},
|
||||
{{pocket_left, 0.}, {pocket_left, -narrow_pocket_depth}},
|
||||
{{pocket_left, -narrow_pocket_depth}, {pocket_right, -narrow_pocket_depth}},
|
||||
{{pocket_right, -narrow_pocket_depth}, {pocket_right, 0.}},
|
||||
{{pocket_right, 0.}, {narrow_pocket_wall_length, 0.}},
|
||||
{{narrow_pocket_wall_length, 0.}, {narrow_pocket_wall_length, -10.}},
|
||||
{{narrow_pocket_wall_length, -10.}, {0., -10.}},
|
||||
{{0., -10.}, {0., 0.}},
|
||||
});
|
||||
const double wall_y = -0.5 * caged_wall_width;
|
||||
const Points wall{Point::new_scale(0., wall_y), Point::new_scale(narrow_pocket_wall_length, wall_y)};
|
||||
|
||||
return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f,
|
||||
dished_min_distance, distance_to_speed);
|
||||
}
|
||||
|
||||
// A cross section that grows a layer's worth on the two faces meeting at either end of a wall, as any
|
||||
// 45 degree overhang does. The wall itself stands on a contour identical to its own, but its ends sit
|
||||
// where the growing faces cut the corners off, and the previous layer's edge there is nearer than the
|
||||
// half line width the centreline is inset by. Both ends therefore read an overhang while everything
|
||||
// between them reads supported: the reverse of the caged span, and the case the sampling above must
|
||||
// leave to the passes after it.
|
||||
constexpr double stepped_wall_inset = 0.5 * caged_wall_width; // mm, centreline inset from the contour
|
||||
constexpr double stepped_end_gap = stepped_wall_inset - caged_layer_height; // mm, how far inside the corner ends up
|
||||
constexpr double stepped_wall_span = 30.; // mm, the length of the wall
|
||||
|
||||
std::vector<ExtendedPoint<2>> sampled_wall_between_growing_corners(const std::function<float(float)>& distance_to_speed)
|
||||
{
|
||||
const AABBTreeLines::LinesDistancer<Linef> prev_layer(std::vector<Linef>{
|
||||
{{0., 0.}, {32., 0.}},
|
||||
{{32., 0.}, {32., -stepped_wall_span}},
|
||||
{{32., -stepped_wall_span}, {0., -stepped_wall_span}},
|
||||
{{0., -stepped_wall_span}, {0., 0.}},
|
||||
});
|
||||
const Points wall{Point::new_scale(stepped_wall_inset, -stepped_end_gap),
|
||||
Point::new_scale(stepped_wall_inset, stepped_end_gap - stepped_wall_span)};
|
||||
|
||||
return estimate_points_properties<true, true, true, true>(wall, prev_layer, caged_wall_width, -1.f,
|
||||
dished_min_distance, distance_to_speed);
|
||||
}
|
||||
|
||||
// How much of a path is printed below the speed a fully supported reading gives. A segment is printed
|
||||
// at the lower of the speeds its ends read.
|
||||
double slowed_length(const std::vector<ExtendedPoint<2>>& points, const std::function<float(float)>& distance_to_speed)
|
||||
{
|
||||
double length = 0.;
|
||||
for (size_t i = 0; i + 1 < points.size(); ++i)
|
||||
if (std::min(distance_to_speed(points[i].distance), distance_to_speed(points[i + 1].distance)) < distance_to_speed(0.f))
|
||||
length += (points[i + 1].position - points[i].position).norm();
|
||||
return length;
|
||||
}
|
||||
|
||||
float furthest_reading(const std::vector<ExtendedPoint<2>>& points)
|
||||
{
|
||||
return std::max_element(points.begin(), points.end(), [](const ExtendedPoint<2>& l, const ExtendedPoint<2>& r) {
|
||||
return l.distance < r.distance;
|
||||
})->distance;
|
||||
}
|
||||
|
||||
DynamicPrintConfig caged_overhang_config(const char* wall_generator){
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{"nozzle_diameter", "0.4"},
|
||||
{"initial_layer_print_height", caged_layer_height},
|
||||
{"layer_height", caged_layer_height},
|
||||
{"line_width", caged_wall_width},
|
||||
{"outer_wall_line_width", caged_wall_width},
|
||||
{"inner_wall_line_width", "0.45"},
|
||||
{"wall_loops", "2"},
|
||||
{"wall_generator", wall_generator},
|
||||
{"wall_sequence", "inner wall/outer wall"},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"detect_overhang_wall", "1"},
|
||||
{"enable_overhang_speed", "1"},
|
||||
{"slowdown_for_curled_perimeters", "0"},
|
||||
{"zaa_enabled", "0"},
|
||||
{"outer_wall_speed", caged_outer_wall_speed},
|
||||
{"inner_wall_speed", "300"},
|
||||
{"overhang_1_4_speed", "0"},
|
||||
{"overhang_2_4_speed", "50"},
|
||||
{"overhang_3_4_speed", "30"},
|
||||
{"overhang_4_4_speed", "10"},
|
||||
{"bridge_speed", "50"},
|
||||
{"filament_max_volumetric_speed", "22"},
|
||||
{"slow_down_for_layer_cooling", "0"},
|
||||
{"slow_down_layers", "0"}, // Nothing but the overhang settings may lower a wall speed
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
std::string caged_overhang_gcode(const char* wall_generator)
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
init_print(std::vector<TriangleMesh>{caged_overhang_mesh()}, print, model, caged_overhang_config(wall_generator), nullptr,
|
||||
false);
|
||||
return gcode(print);
|
||||
}
|
||||
|
||||
// Reports the matched move count alongside the extremes, so a filter that selected nothing is
|
||||
// distinguishable from a span that simply was not slowed.
|
||||
void info_feed_rates(const char* span, const std::vector<double>& feed_rates)
|
||||
{
|
||||
UNSCOPED_INFO("matched " << feed_rates.size() << " " << span << " moves");
|
||||
if (!feed_rates.empty()) {
|
||||
const auto extremes = std::minmax_element(feed_rates.begin(), feed_rates.end());
|
||||
UNSCOPED_INFO("slowest " << *extremes.first / MM_PER_MIN << " mm/s, fastest " << *extremes.second / MM_PER_MIN << " mm/s");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Classic reproduces the endpoint-sampling bug: it emits the span as one long move whose endpoints
|
||||
// both read as supported, so endpoint-only sampling never slows it. Arachne's endpoints already read
|
||||
// as overhanging, but their placement near the cage makes the inferred support vary by layer. Arachne
|
||||
// parity is therefore part of this regression's scope: both generators must classify the unsupported
|
||||
// interior of the same 45-degree span consistently.
|
||||
TEST_CASE("Caged external overhangs are slowed along their span", "[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
const char* wall_generator = GENERATE("classic", "arachne");
|
||||
INFO("wall generator: " << wall_generator);
|
||||
|
||||
const std::vector<double> feed_rates = caged_slope_feed_rates(caged_overhang_gcode(wall_generator));
|
||||
info_feed_rates("caged slope", feed_rates);
|
||||
|
||||
REQUIRE_FALSE(feed_rates.empty());
|
||||
|
||||
// The endpoint bug left Classic at the full wall speed, while Arachne's cage-adjacent endpoint
|
||||
// samples selected much faster bands on some layers. The whole span must stay in the slowed range
|
||||
// for both generators, without requiring their different path segmentations to match.
|
||||
const double fastest = *std::max_element(feed_rates.begin(), feed_rates.end());
|
||||
REQUIRE(fastest < caged_slow_speed * MM_PER_MIN);
|
||||
}
|
||||
|
||||
// The other side of the fix: the midpoint probe fires on every long external perimeter, so a
|
||||
// regression that over-slows would leave the test above green. A fully supported wall must keep the
|
||||
// speed it was configured with.
|
||||
TEST_CASE("Supported vertical walls keep their normal speed", "[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
const char* wall_generator = GENERATE("classic", "arachne");
|
||||
INFO("wall generator: " << wall_generator);
|
||||
|
||||
const std::vector<double> feed_rates = back_wall_feed_rates(caged_overhang_gcode(wall_generator));
|
||||
info_feed_rates("back wall", feed_rates);
|
||||
|
||||
REQUIRE_FALSE(feed_rates.empty());
|
||||
|
||||
const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end());
|
||||
REQUIRE(slowest >= caged_slow_speed * MM_PER_MIN);
|
||||
}
|
||||
|
||||
// The slope's top edge falls mid layer, so the first layer above it still stands 0.179mm proud of the layer
|
||||
// below wherever that layer was still on the slope. That is a real overhang and is slowed, but it ends with the
|
||||
// slope: outside the slope's x range the box runs full height, so the same wall stands on a contour identical to
|
||||
// its own. Sampling the interior of that wall at a single point reported one support reading for all of it and
|
||||
// slowed these fully supported ends along with the rest.
|
||||
TEST_CASE("Wall sections beside a caged overhang keep their normal speed", "[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
const char* wall_generator = GENERATE("classic", "arachne");
|
||||
INFO("wall generator: " << wall_generator);
|
||||
|
||||
const std::vector<double> feed_rates = cage_shoulder_feed_rates(caged_overhang_gcode(wall_generator));
|
||||
info_feed_rates("cage shoulder", feed_rates);
|
||||
|
||||
REQUIRE_FALSE(feed_rates.empty());
|
||||
|
||||
const double slowest = *std::min_element(feed_rates.begin(), feed_rates.end());
|
||||
REQUIRE_THAT(slowest / MM_PER_MIN, Catch::Matchers::WithinRel(caged_outer_wall_speed, 0.01));
|
||||
}
|
||||
|
||||
// A wall is printed at the lower of the speeds its ends read, so a reading only earns a point in the
|
||||
// path where it prints at a different speed from the readings around it. Judging that on the readings
|
||||
// themselves rather than the speeds they produce was too coarse: the configured speeds interpolate
|
||||
// between their sections, so readings a fraction of the slowdown threshold apart still print more than
|
||||
// 10% apart, and a real 45 degree overhang had its true reading dropped as if it agreed with its ends.
|
||||
// The ends then chose the speed on their own, and being next to the walls either side of the overhang
|
||||
// they read differently from layer to layer, banding an overhang that should have been uniform.
|
||||
TEST_CASE("An overhang reading is kept whenever it changes the speed", "[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
// A steep speed curve, of the kind the configured overhang speeds interpolate across.
|
||||
const std::vector<ExtendedPoint<2>> points =
|
||||
sampled_wall_over_dished_layer([](float distance) { return std::round(200.f - 400.f * distance); });
|
||||
|
||||
REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_mid_reading, dished_reading_tolerance));
|
||||
}
|
||||
|
||||
// The complement, and why the readings alone were tempting: a reading that prints at the same speed as
|
||||
// its neighbours cannot change the G-code, so sampling must leave the path alone however far out it is.
|
||||
TEST_CASE("An overhang reading is dropped when the speed is unchanged", "[ExtrusionProcessor]")
|
||||
{
|
||||
// A flat speed curve, of the kind a single configured overhang speed produces.
|
||||
const std::vector<ExtendedPoint<2>> points = sampled_wall_over_dished_layer([](float) { return 50.f; });
|
||||
|
||||
REQUIRE_THAT(furthest_reading(points), Catch::Matchers::WithinAbs(dished_end_reading, dished_reading_tolerance));
|
||||
}
|
||||
|
||||
TEST_CASE("Coarse probing detects an unsupported pocket away from the wall midpoint",
|
||||
"[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
const std::function<float(float)> distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; };
|
||||
const std::vector<ExtendedPoint<2>> points =
|
||||
sampled_wall_over_narrow_pocket(0.25 * narrow_pocket_wall_length, distance_to_speed);
|
||||
const double slowed = slowed_length(points, distance_to_speed);
|
||||
|
||||
REQUIRE(slowed > 0.);
|
||||
REQUIRE(slowed < 5.);
|
||||
}
|
||||
|
||||
TEST_CASE("Coarse probing brackets a narrow slowdown at the wall midpoint",
|
||||
"[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
// Half of the pocket reading still maps to full speed. A matching probe in either half therefore must not
|
||||
// prune that half before a supported point has been found close enough to bracket the slow midpoint.
|
||||
const std::function<float(float)> distance_to_speed = [](float distance) { return distance <= 0.2f ? 100.f : 50.f; };
|
||||
const std::vector<ExtendedPoint<2>> points =
|
||||
sampled_wall_over_narrow_pocket(0.5 * narrow_pocket_wall_length, distance_to_speed);
|
||||
const double slowed = slowed_length(points, distance_to_speed);
|
||||
|
||||
REQUIRE(slowed > 0.);
|
||||
REQUIRE(slowed < 5.);
|
||||
}
|
||||
|
||||
// Sampling probes the interior, so it must not answer for the ends. On a supported wall between two
|
||||
// corners that read an overhang, the reading that differs is the end's own, and the pass that ends a
|
||||
// slowdown an end reads places its point from how far out that end is. Sampling took the difference as
|
||||
// its own to report and put a point at the nearest position bisection had reached instead, which both
|
||||
// sits further along the wall and leaves too little of it for that pass to run on, so the corner
|
||||
// slowdown ran millimetres up an otherwise supported wall. Its length grows with the wall, so on a
|
||||
// model whose cross section keeps growing it reads as a stair stepped band up the corner.
|
||||
TEST_CASE("A supported wall between overhanging corners is slowed no further than its ends require",
|
||||
"[ExtrusionProcessor][Regression]")
|
||||
{
|
||||
// A steep speed curve, so the ends and the interior between them print at clearly different speeds.
|
||||
const std::function<float(float)> distance_to_speed = [](float distance) {
|
||||
return std::round(float(caged_outer_wall_speed) - 400.f * distance);
|
||||
};
|
||||
|
||||
const double sampled = slowed_length(sampled_wall_between_growing_corners(distance_to_speed), distance_to_speed);
|
||||
// The same wall with sampling switched off: what the endpoint driven passes alone make of the corners.
|
||||
const double unsampled = slowed_length(sampled_wall_between_growing_corners({}), distance_to_speed);
|
||||
|
||||
// The corners do read an overhang, so there is a slowdown for sampling to have lengthened.
|
||||
REQUIRE(unsampled > 0.);
|
||||
REQUIRE(sampled <= unsampled);
|
||||
}
|
||||
|
||||
TEST_CASE("Benchmark caged overhang interior sampling", "[ExtrusionProcessor][!benchmark]"){
|
||||
const char* wall_generator = GENERATE("classic", "arachne");
|
||||
|
||||
BENCHMARK(wall_generator)
|
||||
{
|
||||
return caged_overhang_gcode(wall_generator);
|
||||
};
|
||||
}
|
||||
@@ -698,3 +698,294 @@ TEST_CASE("Solid infill direction offsets every layer when no template is set",
|
||||
CHECK(delta == 30);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Honeycomb infill rounds its cell corners with the smooth factor", "[Fill]")
|
||||
{
|
||||
// A cell whose sides are several times the line width, so that the corners have room to be rounded.
|
||||
const double spacing = 0.45;
|
||||
const double density = 0.1;
|
||||
auto fill = [spacing, density](double smooth_factor) {
|
||||
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("honeycomb"));
|
||||
filler->spacing = spacing;
|
||||
|
||||
FillParams params;
|
||||
params.density = float(density);
|
||||
params.dont_adjust = true;
|
||||
// Keep the fragments apart, so that only the turns of the pattern itself are measured.
|
||||
params.anchor_length_max = 0.f;
|
||||
params.smooth_factor = smooth_factor;
|
||||
|
||||
Slic3r::ExPolygon square{ Slic3r::Points{
|
||||
Point::new_scale(0., 0.), Point::new_scale(50., 0.), Point::new_scale(50., 50.), Point::new_scale(0., 50.) } };
|
||||
Slic3r::Surface surface(stInternal, square);
|
||||
return filler->fill_surface(&surface, params);
|
||||
};
|
||||
|
||||
// Cosine of the sharpest turn of any of the paths, 1 meaning none of them turns at all.
|
||||
auto sharpest_turn_cosine = [](const Slic3r::Polylines &polylines) {
|
||||
double sharpest = 1.;
|
||||
for (const Polyline &polyline : polylines)
|
||||
for (size_t i = 1; i + 1 < polyline.size(); ++i) {
|
||||
const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized();
|
||||
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
||||
}
|
||||
return sharpest;
|
||||
};
|
||||
auto point_count = [](const Slic3r::Polylines &polylines) {
|
||||
return std::accumulate(polylines.begin(), polylines.end(), size_t(0),
|
||||
[](size_t count, const Polyline &polyline) { return count + polyline.size(); });
|
||||
};
|
||||
|
||||
const Slic3r::Polylines sharp = fill(0.);
|
||||
const Slic3r::Polylines smooth = fill(1.);
|
||||
|
||||
REQUIRE(!sharp.empty());
|
||||
REQUIRE(smooth.size() == sharp.size());
|
||||
REQUIRE(point_count(smooth) > point_count(sharp));
|
||||
// The cell corners turn by 60 degrees; smoothing replaces them by gentle curves.
|
||||
REQUIRE(sharpest_turn_cosine(sharp) < 0.6);
|
||||
REQUIRE(sharpest_turn_cosine(smooth) > 0.9);
|
||||
}
|
||||
|
||||
// Point count, number of turns sharper than 25 degrees and length of the sparse infill of a print.
|
||||
// A rounded corner is a run of much gentler turns, so smoothing shows up as fewer sharp ones.
|
||||
struct SparseInfillShape {
|
||||
size_t point_count { 0 };
|
||||
size_t sharp_turns { 0 };
|
||||
size_t path_count { 0 };
|
||||
double length { 0. };
|
||||
};
|
||||
|
||||
static SparseInfillShape sparse_infill_shape(const Print &print)
|
||||
{
|
||||
SparseInfillShape shape;
|
||||
|
||||
auto account = [&shape](const ExtrusionPath &path) {
|
||||
if (!sparse_role(path.role()))
|
||||
return;
|
||||
const Points3 &pts = path.polyline.points;
|
||||
++shape.path_count;
|
||||
shape.point_count += pts.size();
|
||||
for (size_t i = 1; i < pts.size(); ++i)
|
||||
shape.length += (pts[i] - pts[i - 1]).head<2>().cast<double>().norm();
|
||||
for (size_t i = 1; i + 1 < pts.size(); ++i) {
|
||||
const Vec2d incoming = (pts[i] - pts[i - 1]).head<2>().cast<double>();
|
||||
const Vec2d outgoing = (pts[i + 1] - pts[i]).head<2>().cast<double>();
|
||||
if (incoming.squaredNorm() > 0. && outgoing.squaredNorm() > 0. &&
|
||||
incoming.normalized().dot(outgoing.normalized()) < 0.9)
|
||||
++shape.sharp_turns;
|
||||
}
|
||||
};
|
||||
|
||||
for (const Layer *layer : print.objects().front()->layers())
|
||||
for (const LayerRegion *region : layer->regions())
|
||||
for (const ExtrusionEntity *entity : region->fills.flatten().entities) {
|
||||
if (auto *path = dynamic_cast<const ExtrusionPath *>(entity))
|
||||
account(*path);
|
||||
else if (auto *multi = dynamic_cast<const ExtrusionMultiPath *>(entity))
|
||||
for (const ExtrusionPath &p : multi->paths)
|
||||
account(p);
|
||||
else if (auto *loop = dynamic_cast<const ExtrusionLoop *>(entity))
|
||||
for (const ExtrusionPath &p : loop->paths)
|
||||
account(p);
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
|
||||
TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]")
|
||||
{
|
||||
auto shape_for = [](const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "lightning"},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for("0%");
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.point_count > 0);
|
||||
// The branch turns are replaced by curves, which cut the corners off and take more points to
|
||||
// describe. The turns where two branches are joined into one path stay sharp.
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
REQUIRE(smooth.length < sharp.length);
|
||||
}
|
||||
|
||||
TEST_CASE("Concentric infill rounds its loops with the smooth factor", "[Fill]")
|
||||
{
|
||||
auto shape_for = [](const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "concentric"},
|
||||
{"sparse_infill_density", "20%"},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for("0%");
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.point_count > 0);
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
REQUIRE(smooth.length < sharp.length);
|
||||
}
|
||||
|
||||
TEST_CASE("Cross hatch infill rounds its transition layers with the smooth factor", "[Fill]")
|
||||
{
|
||||
auto shape_for = [](const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "crosshatch"},
|
||||
{"sparse_infill_density", "20%"},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for("0%");
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.point_count > 0);
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
REQUIRE(smooth.length < sharp.length);
|
||||
}
|
||||
|
||||
TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one line", "[Fill]")
|
||||
{
|
||||
auto shape_for = [](int multiline, const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "grid"},
|
||||
{"sparse_infill_density", "20%"},
|
||||
{"fill_multiline", multiline},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for(2, "0%");
|
||||
const SparseInfillShape smooth = shape_for(2, "100%");
|
||||
|
||||
REQUIRE(sharp.point_count > 0);
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
REQUIRE(smooth.length < sharp.length);
|
||||
|
||||
// A single line per infill wall is the plain crossing line grid, which has no corner of its own.
|
||||
const SparseInfillShape single_sharp = shape_for(1, "0%");
|
||||
const SparseInfillShape single_smooth = shape_for(1, "100%");
|
||||
REQUIRE(single_sharp.point_count > 0);
|
||||
REQUIRE(single_smooth.point_count == single_sharp.point_count);
|
||||
REQUIRE(single_smooth.length == single_sharp.length);
|
||||
}
|
||||
|
||||
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
|
||||
{
|
||||
auto shape_for = [](const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "3dhoneycomb"},
|
||||
{"sparse_infill_density", "20%"},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for("0%");
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.point_count > 0);
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
REQUIRE(smooth.length < sharp.length);
|
||||
}
|
||||
|
||||
TEST_CASE("Smoothed concentric infill stays inside the fill region", "[Fill][Regression]")
|
||||
{
|
||||
// The concentric loops are offsets of the fill region and are never clipped to it, so a corner
|
||||
// rounded across its boundary ends up in a hole or over a wall. Rounding cuts toward the inside of
|
||||
// the turn, which leaves the region at every corner of a hole, and in a region thinner than the
|
||||
// curve even at a corner turning inwards.
|
||||
const bool thin_region = GENERATE(false, true);
|
||||
ExPolygon region;
|
||||
if (thin_region) {
|
||||
// An L of two 1.2mm wide arms: cutting the corner they meet at crosses both of them.
|
||||
region = ExPolygon{ Slic3r::Points{
|
||||
Point::new_scale(0., 0.), Point::new_scale(20., 0.), Point::new_scale(20., 1.2),
|
||||
Point::new_scale(1.2, 1.2), Point::new_scale(1.2, 20.), Point::new_scale(0., 20.) } };
|
||||
} else {
|
||||
region = ExPolygon{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(50., 0.),
|
||||
Point::new_scale(50., 50.), Point::new_scale(0., 50.) },
|
||||
Slic3r::Points{ Point::new_scale(30., 20.), Point::new_scale(30., 30.),
|
||||
Point::new_scale(20., 30.), Point::new_scale(20., 20.) } };
|
||||
}
|
||||
CAPTURE(thin_region);
|
||||
|
||||
auto fill = [®ion](double smooth_factor) {
|
||||
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("concentric"));
|
||||
filler->spacing = 0.45;
|
||||
|
||||
FillParams params;
|
||||
params.density = 0.1f;
|
||||
params.dont_adjust = true;
|
||||
params.smooth_factor = smooth_factor;
|
||||
|
||||
Slic3r::Surface surface(stInternal, region);
|
||||
return filler->fill_surface(&surface, params);
|
||||
};
|
||||
auto point_count = [](const Slic3r::Polylines &polylines) {
|
||||
return std::accumulate(polylines.begin(), polylines.end(), size_t(0),
|
||||
[](size_t count, const Polyline &polyline) { return count + polyline.size(); });
|
||||
};
|
||||
|
||||
const Slic3r::Polylines sharp = fill(0.);
|
||||
const Slic3r::Polylines smooth = fill(1.);
|
||||
REQUIRE(!sharp.empty());
|
||||
|
||||
// Nothing leaves the fill region, which the unrounded loops already touch from the inside.
|
||||
const ExPolygons bounds = offset_ex(region, float(SCALED_EPSILON));
|
||||
REQUIRE(diff_pl(sharp, bounds).empty());
|
||||
REQUIRE(diff_pl(smooth, bounds).empty());
|
||||
// The corners that the region has room for are still rounded.
|
||||
if (!thin_region)
|
||||
REQUIRE(point_count(smooth) > point_count(sharp));
|
||||
}
|
||||
|
||||
TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "[Fill][Regression]")
|
||||
{
|
||||
// With more than one line per infill wall, the branches are printed as outlines drawn around them,
|
||||
// and the outlines of branches that run close to each other merge into one. Rounding the branches
|
||||
// before those outlines are built moves them apart, which breaks the merged outlines up into
|
||||
// separate loops - many more of them, each needing its own travel move.
|
||||
auto shape_for = [](const std::string &smooth_factor) {
|
||||
Print print;
|
||||
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
|
||||
{{"sparse_infill_pattern", "lightning"},
|
||||
{"sparse_infill_density", "50%"},
|
||||
{"fill_multiline", 2},
|
||||
{"sparse_infill_smooth_factor", smooth_factor},
|
||||
{"layer_height", 0.2}});
|
||||
return sparse_infill_shape(print);
|
||||
};
|
||||
|
||||
const SparseInfillShape sharp = shape_for("0%");
|
||||
const SparseInfillShape smooth = shape_for("100%");
|
||||
|
||||
REQUIRE(sharp.path_count > 0);
|
||||
// The loop count varies by a loop or two between platforms and between runs, so this is not an
|
||||
// exact comparison. Smoothing should leave it about where it was; uncapping the smoothing
|
||||
// reach, the regression this guards against, adds about 10%.
|
||||
const size_t allowed_extra = sharp.path_count / 50; // 2%
|
||||
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
|
||||
// The outlines are still rounded.
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
}
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
@@ -418,3 +423,194 @@ TEST_CASE("Per-slot machine limits follow the active nozzle", "[GCodeTiming][Mul
|
||||
REQUIRE_THAT(times[2], Catch::Matchers::WithinRel(101.0 / 200.0, 0.10));
|
||||
}
|
||||
}
|
||||
|
||||
// Junction planning decides the speeds the "actual speed" / "actual flow" preview shows. Per-axis
|
||||
// jerk limits a corner by the largest single-axis component of the velocity change, allowing sqrt(2)
|
||||
// more speed on a diagonal than on an axis -- a four-lobed ripple around every circle. Klipper and
|
||||
// Marlin 2 with M205 J plan with junction deviation instead, which sees only the corner angle.
|
||||
namespace {
|
||||
|
||||
// One acceleration everywhere and axis limits far above it, so only the junction model under test
|
||||
// can slow a corner down.
|
||||
FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity, double junction_deviation)
|
||||
{
|
||||
FullPrintConfig config;
|
||||
config.gcode_flavor.value = flavor;
|
||||
config.filament_diameter.values = {1.75};
|
||||
config.filament_map.values = {1};
|
||||
|
||||
const std::vector<double> accel = {1000.0, 1000.0};
|
||||
const std::vector<double> axis = {20000.0, 20000.0};
|
||||
const std::vector<double> speed = {500.0, 500.0};
|
||||
config.machine_max_acceleration_extruding.values = accel;
|
||||
config.machine_max_acceleration_travel.values = accel;
|
||||
config.machine_max_acceleration_retracting.values = accel;
|
||||
config.machine_max_acceleration_x.values = axis;
|
||||
config.machine_max_acceleration_y.values = axis;
|
||||
config.machine_max_acceleration_z.values = axis;
|
||||
config.machine_max_acceleration_e.values = axis;
|
||||
config.machine_max_speed_x.values = speed;
|
||||
config.machine_max_speed_y.values = speed;
|
||||
config.machine_max_speed_z.values = speed;
|
||||
config.machine_max_speed_e.values = speed;
|
||||
// Klipper reads this as the square corner velocity, Marlin as classic jerk.
|
||||
config.machine_max_jerk_x.values = {corner_velocity, corner_velocity};
|
||||
config.machine_max_jerk_y.values = {corner_velocity, corner_velocity};
|
||||
config.machine_max_jerk_z.values = {corner_velocity, corner_velocity};
|
||||
// Kept out of the way so it never binds in the classic-jerk comparisons.
|
||||
config.machine_max_jerk_e.values = {100.0, 100.0};
|
||||
config.machine_max_junction_deviation.values = {junction_deviation, junction_deviation};
|
||||
config.machine_min_extruding_rate.values = {0.0, 0.0};
|
||||
config.machine_min_travel_rate.values = {0.0, 0.0};
|
||||
return config;
|
||||
}
|
||||
|
||||
constexpr double junction_x = 60.0;
|
||||
constexpr double junction_y = 60.0;
|
||||
|
||||
// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`.
|
||||
// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests
|
||||
// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric
|
||||
// as the formulas below assume.
|
||||
std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0)
|
||||
{
|
||||
const double len = 40.0;
|
||||
const double a_in = orientation_deg * M_PI / 180.0;
|
||||
const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0;
|
||||
std::ostringstream extrude;
|
||||
if (e_per_mm > 0.0)
|
||||
extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm;
|
||||
|
||||
std::ostringstream os;
|
||||
os << std::fixed << std::setprecision(4)
|
||||
<< "M83\n"
|
||||
<< "G1 Z0.2 F1200\n"
|
||||
<< "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n"
|
||||
<< "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n"
|
||||
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out)
|
||||
<< extrude.str() << " F9000\n";
|
||||
return os.str();
|
||||
}
|
||||
|
||||
// Speed allowed through the corner: the vertex ending the incoming move carries that block's exit
|
||||
// speed, and the vertices the actual-speed pass inserts are all strictly interior.
|
||||
double corner_speed(const GCodeProcessorResult& r)
|
||||
{
|
||||
for (const auto& mv : r.moves)
|
||||
if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) &&
|
||||
std::abs(mv.position.x() - junction_x) < 1e-3 &&
|
||||
std::abs(mv.position.y() - junction_y) < 1e-3)
|
||||
return mv.actual_feedrate;
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation,
|
||||
double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0)
|
||||
{
|
||||
GCodeProcessor proc;
|
||||
run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation),
|
||||
corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str());
|
||||
return corner_speed(proc.get_result());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
// jd = scv^2 * (sqrt(2) - 1) / max_accel, then v^2 = jd * accel * sin(t/2) / (1 - sin(t/2)).
|
||||
// The acceleration cancels: the corner speed depends only on the scv and the angle.
|
||||
const double scv = 5.0;
|
||||
|
||||
SECTION("a right angle is taken at exactly the square corner velocity") {
|
||||
// sin(t/2) = sqrt(0.5) at 90 degrees, so v == scv -- the definition of the square corner
|
||||
// velocity, and what makes the mapping above the right one.
|
||||
REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, 90.0), Catch::Matchers::WithinRel(scv, 0.02));
|
||||
}
|
||||
|
||||
SECTION("a shallow corner is taken far faster than the per-axis jerk model allows") {
|
||||
// 6 degrees: sin(t/2) = cos(3 deg), so v = 5 * sqrt((sqrt(2) - 1) * 728.68) = 86.9mm/s. Per-axis
|
||||
// jerk ignores the angle and caps the velocity *change* (2v*sin(3 deg)), giving 47.8mm/s.
|
||||
const double jd_speed = planned_corner_speed(gcfKlipper, scv, 0.0, 6.0);
|
||||
const double jerk_speed = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, 6.0);
|
||||
REQUIRE_THAT(jd_speed, Catch::Matchers::WithinRel(86.87, 0.02));
|
||||
REQUIRE_THAT(jerk_speed, Catch::Matchers::WithinRel(47.75, 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Junction deviation limits a corner by its angle alone, not by its orientation",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
// The four-lobed ripple on circular walls is per-axis jerk being anisotropic: a velocity change
|
||||
// lying on an axis gets sqrt(2) less headroom than the same change on the diagonal.
|
||||
const double scv = 5.0;
|
||||
const double turn = 6.0;
|
||||
|
||||
SECTION("Klipper plans both orientations identically") {
|
||||
const double on_axis = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0);
|
||||
const double diagonal = planned_corner_speed(gcfKlipper, scv, 0.0, turn, 45.0);
|
||||
REQUIRE(on_axis > 0.0);
|
||||
REQUIRE_THAT(diagonal, Catch::Matchers::WithinRel(on_axis, 0.02));
|
||||
}
|
||||
|
||||
SECTION("the classic jerk model keeps its orientation dependence") {
|
||||
const double on_axis = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 0.0);
|
||||
const double diagonal = planned_corner_speed(gcfMarlinLegacy, scv, 0.0, turn, 45.0);
|
||||
REQUIRE(on_axis > 0.0);
|
||||
REQUIRE(diagonal / on_axis > 1.2);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Junction deviation is only used where the firmware actually plans with it",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
const double jerk = 5.0;
|
||||
|
||||
SECTION("Marlin 2 with M205 J disabled keeps the classic jerk planning") {
|
||||
// machine_max_junction_deviation == 0 is how a Marlin 2 printer says it runs classic jerk.
|
||||
const double classic = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0);
|
||||
REQUIRE(classic > 0.0);
|
||||
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.0, 90.0),
|
||||
Catch::Matchers::WithinRel(classic, 1e-4));
|
||||
}
|
||||
|
||||
SECTION("Marlin 2 with M205 J enabled switches to junction deviation") {
|
||||
// sqrt(1000 * 0.05 * 2.4142136) = 11.0mm/s, independent of the jerk values it no longer reads.
|
||||
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, jerk, 0.05, 90.0),
|
||||
Catch::Matchers::WithinRel(10.99, 0.02));
|
||||
}
|
||||
|
||||
SECTION("machines without junction deviation are untouched by the jerk values it would ignore") {
|
||||
// A flavor that never enters the junction deviation path must ignore the setting entirely.
|
||||
const double without = planned_corner_speed(gcfMarlinLegacy, jerk, 0.0, 90.0);
|
||||
REQUIRE_THAT(planned_corner_speed(gcfMarlinLegacy, jerk, 0.05, 90.0),
|
||||
Catch::Matchers::WithinRel(without, 1e-4));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it",
|
||||
"[GCodeTiming][JunctionDeviation]")
|
||||
{
|
||||
// The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the
|
||||
// E term makes the two paths look more parallel than they are and the corner comes out too fast,
|
||||
// the more so the higher the flow. Marlin normalizes over XYZE on any extruding move
|
||||
// (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether
|
||||
// (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone.
|
||||
const double scv = 5.0;
|
||||
const double turn = 6.0;
|
||||
const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn);
|
||||
REQUIRE(geometric > 0.0);
|
||||
|
||||
// 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one.
|
||||
// Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9.
|
||||
for (double e_per_mm : {0.029, 0.1})
|
||||
REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm),
|
||||
Catch::Matchers::WithinRel(geometric, 0.02));
|
||||
|
||||
SECTION("and the same holds on Marlin 2") {
|
||||
const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn);
|
||||
REQUIRE(marlin > 0.0);
|
||||
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029),
|
||||
Catch::Matchers::WithinRel(marlin, 0.02));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_preset_setting_id.cpp
|
||||
test_filament_id_succession.cpp
|
||||
test_preset_diff.cpp
|
||||
test_vendor_cache.cpp
|
||||
test_elephant_foot_compensation.cpp
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_fill_plane_path.cpp
|
||||
test_geometry.cpp
|
||||
test_multimaterial_segmentation.cpp
|
||||
|
||||
@@ -235,6 +235,56 @@ SCENARIO("Config ini load/save interface", "[Config]") {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Flush-volume warning predicate respects used filament transitions", "[Config][Regression]")
|
||||
{
|
||||
const std::vector<double> multipliers = {1.0};
|
||||
|
||||
SECTION("Single used filament does not trigger warning with zero transition entries")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 0.0,
|
||||
0.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1};
|
||||
|
||||
REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Two used filaments trigger warning when transition flush entry is zero")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 0.0,
|
||||
0.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Two used filaments do not trigger warning when transitions are non-zero")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 280.0,
|
||||
280.0, 0.0
|
||||
};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE_FALSE(has_zero_flush_volume_for_used_filaments(matrix, multipliers, used_filaments));
|
||||
}
|
||||
|
||||
SECTION("Zero multiplier still triggers warning when multiple filaments are used")
|
||||
{
|
||||
const std::vector<double> matrix = {
|
||||
0.0, 280.0,
|
||||
280.0, 0.0
|
||||
};
|
||||
const std::vector<double> zero_multiplier = {0.0};
|
||||
const std::vector<int> used_filaments = {1, 2};
|
||||
|
||||
REQUIRE(has_zero_flush_volume_for_used_filaments(matrix, zero_multiplier, used_filaments));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: https://github.com/SoftFever/OrcaSlicer/issues/11269 - Is this test still relevant? Delete if not.
|
||||
// It was failing so at least "nozzle_type" and "extruder_printable_area" could not be serialized
|
||||
// and an exception was thrown, but "nozzle_type" has been around for at least 3 months now.
|
||||
|
||||
173
tests/libslic3r/test_fill_corner_smoothing.cpp
Normal file
173
tests/libslic3r/test_fill_corner_smoothing.cpp
Normal file
@@ -0,0 +1,173 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
#include "libslic3r/Fill/FillCornerSmoothing.hpp"
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
// A right angle turn, with the outgoing leg ten times longer than the incoming one.
|
||||
Polyline asymmetric_corner()
|
||||
{
|
||||
return Polyline{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 100.) };
|
||||
}
|
||||
|
||||
double max_turn_cosine(const Polyline &polyline)
|
||||
{
|
||||
double sharpest = 1.;
|
||||
for (size_t i = 1; i + 1 < polyline.size(); ++i) {
|
||||
const Vec2d incoming = (polyline[i] - polyline[i - 1]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (polyline[i + 1] - polyline[i]).cast<double>().normalized();
|
||||
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
||||
}
|
||||
return sharpest;
|
||||
}
|
||||
|
||||
bool contains(const Polyline &polyline, const Point &point)
|
||||
{
|
||||
return std::find(polyline.points.begin(), polyline.points.end(), point) != polyline.points.end();
|
||||
}
|
||||
|
||||
const double tolerance = scaled<double>(0.0125);
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Corner smoothing replaces a sharp vertex by a curve", "[FillCornerSmoothing]")
|
||||
{
|
||||
const Polyline sharp = asymmetric_corner();
|
||||
Polyline smooth = sharp;
|
||||
smooth_polyline_corners(smooth, 1., tolerance);
|
||||
|
||||
REQUIRE(smooth.size() > sharp.size());
|
||||
REQUIRE(smooth.front() == sharp.front());
|
||||
REQUIRE(smooth.back() == sharp.back());
|
||||
// The right angle is gone, every remaining turn is a gentle one.
|
||||
REQUIRE(max_turn_cosine(sharp) < 0.1);
|
||||
REQUIRE(max_turn_cosine(smooth) > 0.9);
|
||||
REQUIRE(smooth.length() < sharp.length());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing keeps the path untouched at a zero factor", "[FillCornerSmoothing]")
|
||||
{
|
||||
const Polyline sharp = asymmetric_corner();
|
||||
|
||||
Polyline none = sharp;
|
||||
smooth_polyline_corners(none, 0., tolerance);
|
||||
REQUIRE(none.points == sharp.points);
|
||||
|
||||
Polyline invalid = sharp;
|
||||
smooth_polyline_corners(invalid, std::numeric_limits<double>::quiet_NaN(), tolerance);
|
||||
REQUIRE(invalid.points == sharp.points);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing consumes at most half of the shorter leg", "[FillCornerSmoothing]")
|
||||
{
|
||||
// The curve must not reach beyond the middle of either adjoining segment, otherwise the curves of
|
||||
// two adjacent corners would overlap. The shorter leg is 10mm long, so the corner at (10, 0) is
|
||||
// left 5mm before it and rejoined 5mm past it, even though the other leg is 100mm long.
|
||||
Polyline smooth = asymmetric_corner();
|
||||
smooth_polyline_corners(smooth, 1., tolerance);
|
||||
|
||||
REQUIRE(contains(smooth, Point::new_scale(5., 0.)));
|
||||
REQUIRE(contains(smooth, Point::new_scale(10., 5.)));
|
||||
// A Bezier curve stays within the convex hull of its control points, so the rounded path stays
|
||||
// inside the box spanned by the two legs.
|
||||
for (const Point &point : smooth.points) {
|
||||
REQUIRE(point.x() >= 0);
|
||||
REQUIRE(point.y() >= 0);
|
||||
REQUIRE(point.x() <= Point::new_scale(10., 0.).x());
|
||||
REQUIRE(point.y() <= Point::new_scale(0., 100.).y());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing scales the curve with the factor", "[FillCornerSmoothing]")
|
||||
{
|
||||
Polyline half = asymmetric_corner();
|
||||
smooth_polyline_corners(half, 0.5, tolerance);
|
||||
Polyline full = asymmetric_corner();
|
||||
smooth_polyline_corners(full, 1., tolerance);
|
||||
|
||||
// Half of the factor leaves the 10mm leg half as far from the corner.
|
||||
REQUIRE(contains(half, Point::new_scale(7.5, 0.)));
|
||||
REQUIRE(contains(full, Point::new_scale(5., 0.)));
|
||||
// A larger factor rounds a wider portion of the legs, cutting more of the corner off.
|
||||
REQUIRE(full.length() < half.length());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing leaves hairpins sharp", "[FillCornerSmoothing]")
|
||||
{
|
||||
// Both ends of a curve replacing a nearly reversing turn coincide, which would round the hairpin
|
||||
// into a degenerate loop instead of a tip.
|
||||
Polyline hairpin{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(0., 0.5) };
|
||||
const Polyline sharp = hairpin;
|
||||
smooth_polyline_corners(hairpin, 1., tolerance);
|
||||
REQUIRE(hairpin == sharp);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing follows the flattening tolerance", "[FillCornerSmoothing]")
|
||||
{
|
||||
Polyline coarse = asymmetric_corner();
|
||||
smooth_polyline_corners(coarse, 1., scaled<double>(0.2));
|
||||
Polyline fine = asymmetric_corner();
|
||||
smooth_polyline_corners(fine, 1., scaled<double>(0.001));
|
||||
|
||||
REQUIRE(fine.size() > coarse.size());
|
||||
REQUIRE(fine.front() == coarse.front());
|
||||
REQUIRE(fine.back() == coarse.back());
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing emits no zero length segments", "[FillCornerSmoothing]")
|
||||
{
|
||||
// Fully smoothed adjacent corners meet at the midpoint of the segment they share.
|
||||
Polyline zigzag;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
zigzag.points.emplace_back(Point::new_scale(i, i % 2 ? 1. : 0.));
|
||||
smooth_polyline_corners(zigzag, 1., tolerance);
|
||||
|
||||
for (size_t i = 1; i < zigzag.size(); ++i)
|
||||
REQUIRE((zigzag[i] - zigzag[i - 1]).cast<double>().squaredNorm() > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing rounds every vertex of a polygon", "[FillCornerSmoothing]")
|
||||
{
|
||||
// A polygon closes implicitly, so none of its corners may stay sharp, not even the first one.
|
||||
const Polygon square{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.),
|
||||
Point::new_scale(0., 10.) };
|
||||
Polygons smooth{ square };
|
||||
smooth_polygons_corners(smooth, 1., tolerance);
|
||||
const Polyline rounded = smooth.front().split_at_first_point();
|
||||
|
||||
REQUIRE(smooth.front().size() > square.size());
|
||||
REQUIRE(max_turn_cosine(rounded) > 0.9);
|
||||
// The turn from the closing segment back into the first one must be gentle as well.
|
||||
const Vec2d incoming = (rounded[rounded.size() - 1] - rounded[rounded.size() - 2]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (rounded[1] - rounded[0]).cast<double>().normalized();
|
||||
REQUIRE(incoming.dot(outgoing) > 0.9);
|
||||
// None of the corners is cut by more than half of a 10mm side.
|
||||
for (const Point &point : smooth.front().points) {
|
||||
REQUIRE(point.x() >= 0);
|
||||
REQUIRE(point.y() >= 0);
|
||||
REQUIRE(point.x() <= Point::new_scale(10., 0.).x());
|
||||
REQUIRE(point.y() <= Point::new_scale(0., 10.).y());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Corner smoothing keeps the ends of a path that returns to its start", "[FillCornerSmoothing][Regression]")
|
||||
{
|
||||
// A branch of a lightning tree walks out and retraces its way back, ending where it started. Its
|
||||
// ends are two free ends that happen to coincide, and joining them would close it into a loop.
|
||||
Polyline retrace{ Point::new_scale(0., 0.), Point::new_scale(10., 0.), Point::new_scale(10., 10.),
|
||||
Point::new_scale(5., 10.), Point::new_scale(0., 0.) };
|
||||
const Polyline sharp = retrace;
|
||||
smooth_polyline_corners(retrace, 1., tolerance);
|
||||
|
||||
REQUIRE(retrace.size() > sharp.size());
|
||||
REQUIRE(retrace.front() == sharp.front());
|
||||
REQUIRE(retrace.back() == sharp.back());
|
||||
}
|
||||
@@ -27,6 +27,31 @@ public:
|
||||
}
|
||||
};
|
||||
|
||||
class TestableOctagramSpiral : public FillOctagramSpiral
|
||||
{
|
||||
public:
|
||||
Points generate_points(double resolution, double smooth_factor = 0., coord_t max_coordinate = 7)
|
||||
{
|
||||
InfillPolylineOutput output(output_scale);
|
||||
FillParams params;
|
||||
params.smooth_factor = smooth_factor;
|
||||
FillOctagramSpiral::generate(-max_coordinate, -max_coordinate, max_coordinate, max_coordinate, resolution, params, output);
|
||||
return std::move(output.result());
|
||||
}
|
||||
};
|
||||
|
||||
// Cosine of the sharpest turn of a path, 1 meaning it has no turn at all.
|
||||
double sharpest_turn_cosine(const Points &points)
|
||||
{
|
||||
double sharpest = 1.;
|
||||
for (size_t i = 1; i + 1 < points.size(); ++i) {
|
||||
const Vec2d incoming = (points[i] - points[i - 1]).cast<double>().normalized();
|
||||
const Vec2d outgoing = (points[i + 1] - points[i]).cast<double>().normalized();
|
||||
sharpest = std::min(sharpest, incoming.dot(outgoing));
|
||||
}
|
||||
return sharpest;
|
||||
}
|
||||
|
||||
double path_length(const Points &points)
|
||||
{
|
||||
double length = 0.;
|
||||
@@ -146,6 +171,35 @@ TEST_CASE("Hilbert smoothing joins straight segments with continuous curvature",
|
||||
REQUIRE(fine_entry_curvature < 0.25 * coarse_entry_curvature);
|
||||
}
|
||||
|
||||
TEST_CASE("Octagram spiral smoothing rounds the turns of the spiral", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableOctagramSpiral().generate_points(0.005);
|
||||
const Points smooth = TestableOctagramSpiral().generate_points(0.005, 1.);
|
||||
|
||||
REQUIRE(smooth.size() > sharp.size());
|
||||
REQUIRE(smooth.front() == sharp.front());
|
||||
REQUIRE(smooth.back() == sharp.back());
|
||||
// The spiral alternates between 90 and 135 degree turns; both are rounded into gentle ones.
|
||||
REQUIRE(sharpest_turn_cosine(sharp) < -0.7);
|
||||
REQUIRE(sharpest_turn_cosine(smooth) > 0.9);
|
||||
|
||||
for (size_t i = 1; i < smooth.size(); ++i)
|
||||
REQUIRE((smooth[i] - smooth[i - 1]).cast<double>().squaredNorm() > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("Octagram spiral smooth factor controls corner curvature", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableOctagramSpiral().generate_points(0.005);
|
||||
const Points half_smooth = TestableOctagramSpiral().generate_points(0.005, 0.5);
|
||||
const Points full_smooth = TestableOctagramSpiral().generate_points(0.005, 1.);
|
||||
const Points invalid_factor = TestableOctagramSpiral().generate_points(
|
||||
0.005, std::numeric_limits<double>::quiet_NaN());
|
||||
|
||||
REQUIRE(path_length(full_smooth) < path_length(half_smooth));
|
||||
REQUIRE(path_length(half_smooth) < path_length(sharp));
|
||||
REQUIRE(invalid_factor == sharp);
|
||||
}
|
||||
|
||||
TEST_CASE("Hilbert curve smooth factor controls corner curvature", "[FillPlanePath]")
|
||||
{
|
||||
const Points sharp = TestableHilbertCurve().generate_points(0.005);
|
||||
|
||||
@@ -574,11 +574,6 @@ TEST_CASE("Convex polygon intersection on two squares touching one vertex", "[Ge
|
||||
Polygon B = A;
|
||||
B.translate(10 / SCALING_FACTOR, 10 / SCALING_FACTOR);
|
||||
|
||||
SVG svg{std::string("one_vertex_touch") + ".svg"};
|
||||
svg.draw(A, "blue");
|
||||
svg.draw(B, "green");
|
||||
svg.Close();
|
||||
|
||||
bool is_inters = Geometry::convex_polygons_intersect(A, B);
|
||||
|
||||
REQUIRE(is_inters == false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <fstream>
|
||||
|
||||
#include "libslic3r/PresetBundle.hpp"
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
@@ -132,7 +133,7 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
VendorProfile orca_vendor("ORCA");
|
||||
VendorProfile orca_vendor; orca_vendor.id = "ORCA";
|
||||
VendorProfile::PrinterModel model;
|
||||
model.name = "Orca Test";
|
||||
orca_vendor.models.emplace_back(model);
|
||||
@@ -143,6 +144,31 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl
|
||||
CHECK(bundle.get_current_vendor_type() == VendorType::Unknown);
|
||||
}
|
||||
|
||||
TEST_CASE("A malformed entry in a vendor's preset list is counted, not thrown", "[Preset][Bundle]")
|
||||
{
|
||||
ScopedTemporaryDir dir;
|
||||
|
||||
// A bare number where the list wants an object. An array element has no key,
|
||||
// so reporting one as if it did throws nlohmann's invalid_iterator - which is
|
||||
// not a parse_error, and escapes the catch around the vendor profile parse.
|
||||
std::ofstream((dir.path() / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","process_list":[123,)"
|
||||
<< R"({"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})";
|
||||
fs::create_directories(dir.path() / "Acme" / "process");
|
||||
std::ofstream((dir.path() / "Acme" / "process" / "standard.json").string())
|
||||
<< R"({"type":"process","name":"0.20mm Standard @Acme","from":"system",)"
|
||||
<< R"("instantiation":"true","layer_height":"0.2"})";
|
||||
|
||||
PresetBundle bundle;
|
||||
size_t loaded = 0;
|
||||
REQUIRE_NOTHROW(loaded = bundle.load_vendor_configs_from_json(
|
||||
dir.path().string(), "Acme", PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent).second);
|
||||
|
||||
CHECK(bundle.error_count() > 0); // the malformed element was counted
|
||||
CHECK(loaded == 1); // the well-formed one beside it still loaded
|
||||
}
|
||||
|
||||
TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][Bundle]")
|
||||
{
|
||||
PresetBundle bundle;
|
||||
|
||||
1620
tests/libslic3r/test_vendor_cache.cpp
Normal file
1620
tests/libslic3r/test_vendor_cache.cpp
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user