mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-10 02:37:04 +00:00
Merge branch 'main' into feature/filament_id
This commit is contained in:
@@ -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