mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 05:27:50 +00:00
Merge main
This commit is contained in:
@@ -574,6 +574,9 @@ static DynamicPrintConfig dual_extruder_toolchange_config()
|
||||
config.set_key_value("nozzle_temperature_range_high", new ConfigOptionInts({240, 240}));
|
||||
config.set_key_value("flush_multiplier", new ConfigOptionFloats({1}));
|
||||
config.set_key_value("flush_volumes_matrix", new ConfigOptionFloats({0, 140, 140, 0}));
|
||||
// Inside the 200x200 test bed; the default y, 220, is not, and generation rejects that.
|
||||
config.set_key_value("wipe_tower_x", new ConfigOptionFloats({50.}));
|
||||
config.set_key_value("wipe_tower_y", new ConfigOptionFloats({50.}));
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
|
||||
@@ -33,10 +35,13 @@ TriangleMesh scaled(TestMesh id, float scale)
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// `extra` is applied last, so a caller can add or override any key.
|
||||
void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, const char *style,
|
||||
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0)
|
||||
int threshold_angle = 30, int build_plate_only = 0, int raft_layers = 0,
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
|
||||
{
|
||||
Slic3r::Test::init_and_process_print({ mesh }, print, {
|
||||
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "enable_support", 1 },
|
||||
{ "support_type", "tree(auto)" },
|
||||
{ "support_style", style },
|
||||
@@ -45,6 +50,8 @@ void slice_with_tree_support(const TriangleMesh &mesh, Slic3r::Print &print, con
|
||||
{ "raft_layers", raft_layers },
|
||||
{ "layer_height", 0.2 },
|
||||
});
|
||||
config.set_deserialize_strict(extra);
|
||||
Slic3r::Test::init_and_process_print({ mesh }, print, config);
|
||||
}
|
||||
|
||||
Points support_points(const Slic3r::Print &print)
|
||||
@@ -63,6 +70,32 @@ size_t support_point_count(const TriangleMesh &mesh, const char *style, int thre
|
||||
return support_points(print).size();
|
||||
}
|
||||
|
||||
// Index of the first differing point, or the common length when they match. An index keeps a
|
||||
// failure readable; comparing the vectors themselves dumps thousands of points.
|
||||
size_t first_difference(const Points &a, const Points &b)
|
||||
{
|
||||
const size_t common = std::min(a.size(), b.size());
|
||||
for (size_t i = 0; i < common; ++i)
|
||||
if (a[i] != b[i])
|
||||
return i;
|
||||
return common;
|
||||
}
|
||||
|
||||
// Slice `mesh` twice and require an identical support point sequence. Point counts and total
|
||||
// length are order insensitive, so the sequence is what a reordering shows up in.
|
||||
void sliced_twice_matches(const TriangleMesh &mesh, int build_plate_only, const char *style = "tree_slim",
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
|
||||
{
|
||||
Slic3r::Print first_print, second_print;
|
||||
slice_with_tree_support(mesh, first_print, style, 30, build_plate_only, 0, extra);
|
||||
slice_with_tree_support(mesh, second_print, style, 30, build_plate_only, 0, extra);
|
||||
const Points first = support_points(first_print);
|
||||
const Points second = support_points(second_print);
|
||||
REQUIRE(first.size() > 1000); // without support the comparison below passes vacuously
|
||||
REQUIRE(second.size() == first.size());
|
||||
REQUIRE(first_difference(first, second) == first.size());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Tree support is generated for an overhang and not for a plain cube", "[TreeSupport]")
|
||||
@@ -123,3 +156,35 @@ TEST_CASE("A raft is still generated under tree support", "[TreeSupport]")
|
||||
// The raft goes under the object.
|
||||
REQUIRE(rafted_object->layers().front()->print_z > unrafted_object->layers().front()->print_z);
|
||||
}
|
||||
|
||||
// drop_nodes() decides the node merges and spawns the next layer's nodes in parallel. Every one of
|
||||
// those decisions has to be applied in a fixed order, or the same model gives different branches on
|
||||
// each slice.
|
||||
TEST_CASE("Tree support toolpaths do not depend on thread scheduling", "[TreeSupport][Regression]")
|
||||
{
|
||||
// Scaled up so that a layer holds enough nodes for the parallel range to be split. At stock
|
||||
// size it stays in one chunk and the order never varies.
|
||||
SECTION("overhang") { sliced_twice_matches(scaled(TestMesh::overhang, 2.f), 0); }
|
||||
SECTION("bridge with hole") { sliced_twice_matches(scaled(TestMesh::bridge_with_hole, 3.f), 0); }
|
||||
// Dropping every branch that cannot reach the bed leaves the survivors dense enough that the
|
||||
// neighbour merge fires in bulk.
|
||||
SECTION("on the build plate") { sliced_twice_matches(scaled(TestMesh::overhang, 4.f), 1); }
|
||||
// Branches resting on the model are what put nodes in a part group other than 0, which is the
|
||||
// only way to reach the prune in the second pass. tree_hybrid additionally builds polygon
|
||||
// nodes, so it is the only style that exercises the overhang merge.
|
||||
SECTION("resting on the model") { sliced_twice_matches(two_tier_mesh(), 0); }
|
||||
SECTION("hybrid on the model") { sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid"); }
|
||||
}
|
||||
|
||||
// Prim breaks equal-distance ties by heap address. A 1 mm branch diameter puts neighbours close
|
||||
// enough to tie, and an explicit line width pins max_move_dist, so the moved tie winner reaches
|
||||
// the support toolpaths.
|
||||
TEST_CASE("Tree support toolpaths do not depend on the MST tie order", "[TreeSupport][Regression]")
|
||||
{
|
||||
sliced_twice_matches(two_tier_mesh(), 0, "tree_hybrid", {
|
||||
{ "tree_support_branch_diameter", 1.0 },
|
||||
{ "tree_support_branch_distance", 5.0 },
|
||||
{ "tree_support_branch_angle", 40 },
|
||||
{ "support_line_width", 0.4 },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,6 +152,8 @@ static DynamicPrintConfig wipe_tower_toolchange_config(const std::string &gcode_
|
||||
{ "outer_wall_filament_id", 2 },
|
||||
{ "inner_wall_filament_id", 2 },
|
||||
{ "enable_prime_tower", true },
|
||||
{ "wipe_tower_x", 50 }, // inside the 200x200 test bed
|
||||
{ "wipe_tower_y", 50 }, // (the default y, 220, is not)
|
||||
{ "layer_height", 0.3 },
|
||||
{ "gcode_flavor", gcode_flavor },
|
||||
});
|
||||
@@ -182,3 +184,148 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
|
||||
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring(unexpected));
|
||||
}
|
||||
}
|
||||
|
||||
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
|
||||
// they call the estimator directly. The estimate counts the filaments the print really uses,
|
||||
// so the two-filament shape gives the outer wall the second one.
|
||||
static DynamicPrintConfig tower_estimate_config(const char *wall_type, unsigned int filaments = 2)
|
||||
{
|
||||
// 100 mm3 per purge on a 50 mm wide tower: one purge is 100/(layer_height * 50) of depth.
|
||||
return multifilament_config(filaments, {
|
||||
{ "outer_wall_filament_id", filaments == 2 ? "2" : "1" },
|
||||
{ "enable_prime_tower", "1" },
|
||||
{ "wipe_tower_wall_type", wall_type },
|
||||
{ "prime_tower_width", "50" },
|
||||
{ "prime_volume", "100" },
|
||||
{ "prime_tower_infill_gap", "100%" },
|
||||
{ "prime_tower_brim_width", "3" },
|
||||
{ "purge_in_prime_tower", "0" },
|
||||
{ "single_extruder_multi_material", "0" },
|
||||
{ "timelapse_type", "0" },
|
||||
{ "layer_height", "0.2" },
|
||||
{ "enable_wrapping_detection", "0" },
|
||||
{ "raft_layers", "0" } });
|
||||
}
|
||||
|
||||
TEST_CASE("The tower is sized for the thinnest layer any object on the plate is sliced at", "[WipeTower]")
|
||||
{
|
||||
// The tower has to survive its thinnest layer, so an override finer than the preset drives
|
||||
// the estimate even on the second object. Two 20 mm cubes, the second at 0.1 mm.
|
||||
const DynamicPrintConfig config = tower_estimate_config("rectangle");
|
||||
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides = {
|
||||
{}, { { "layer_height", "0.1" } } };
|
||||
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20), cube(20) }, print, model, config, &overrides);
|
||||
|
||||
// One purge at 0.1 mm: 100 / (0.1 * 50) = 20 mm, above the 20 mm-tall tower's stability
|
||||
// floor. At the preset's 0.2 mm it would be half that, so the two are easy to tell apart.
|
||||
const float floor_20mm = WipeTower::get_limit_depth_by_height(20.f);
|
||||
REQUIRE(floor_20mm < 10.f);
|
||||
CHECK_THAT(print.wipe_tower_data(2).depth, Catch::Matchers::WithinAbs(20., 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("Validation is given the tower's effective width, not the configured one", "[WipeTower]")
|
||||
{
|
||||
// A rib wall squares the tower, so its width is its depth. Validation reads this rather
|
||||
// than re-deriving the rule from the wall type.
|
||||
Print print;
|
||||
Model model;
|
||||
|
||||
SECTION("a rectangle wall keeps the configured width") {
|
||||
const DynamicPrintConfig config = tower_estimate_config("rectangle");
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
const WipeTowerData &data = print.wipe_tower_data(2);
|
||||
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(50., 1e-4));
|
||||
CHECK(data.depth < data.width);
|
||||
}
|
||||
|
||||
SECTION("a rib wall reports the squared footprint") {
|
||||
const DynamicPrintConfig config = tower_estimate_config("rib");
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
const WipeTowerData &data = print.wipe_tower_data(2);
|
||||
CHECK_THAT(data.width, Catch::Matchers::WithinAbs(data.depth, 1e-4));
|
||||
CHECK(data.width > 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Generating the tower keeps its reported width current", "[WipeTower]")
|
||||
{
|
||||
// width is handed out after the slice, so leaving it at the estimate reports a zero-width
|
||||
// tower to every post-generation consumer.
|
||||
const DynamicPrintConfig config = wipe_tower_toolchange_config("marlin");
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(10) }, print, model, config);
|
||||
print.apply(model, config);
|
||||
REQUIRE(print.wipe_tower_data(2).width > 0.f);
|
||||
|
||||
print.process();
|
||||
REQUIRE(print.is_step_done(psWipeTower));
|
||||
const WipeTowerData &data = print.wipe_tower_data();
|
||||
// A width the generator never wrote reads as zero. A rib wall squares the tower, so the
|
||||
// generated width is the body square: under the configured 50 mm, and inside the depth.
|
||||
CHECK(data.width > 0.f);
|
||||
CHECK(data.width < 50.f);
|
||||
CHECK(data.width <= data.depth + EPSILON);
|
||||
}
|
||||
|
||||
TEST_CASE("A single-filament plate reserves a tower only when one is actually printed", "[WipeTower]")
|
||||
{
|
||||
// The estimate has to answer this the way Print::apply does: reporting no tower for one
|
||||
// that is built collapses the validation hull to a point, and reporting one for a tower
|
||||
// that is not built takes that bed area away from the arranger and draws a preview box
|
||||
// over nothing.
|
||||
Print print;
|
||||
Model model;
|
||||
|
||||
SECTION("no tool change and nothing else that prints one") {
|
||||
const DynamicPrintConfig config = tower_estimate_config("rib", 1);
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
REQUIRE_FALSE(print.has_wipe_tower());
|
||||
CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6));
|
||||
}
|
||||
|
||||
// A raft puts the tower on every layer below the object, but only where there is a tower:
|
||||
// Print::apply runs normalize_fdm_2, which clears enable_prime_tower for a plate that
|
||||
// purges one filament and has neither smooth timelapse nor wrapping detection on.
|
||||
SECTION("a raft alone does not print one") {
|
||||
DynamicPrintConfig config = tower_estimate_config("rib", 1);
|
||||
config.set_deserialize_strict({ { "raft_layers", "3" } });
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
REQUIRE_FALSE(print.config().enable_prime_tower.value);
|
||||
REQUIRE_FALSE(print.has_wipe_tower());
|
||||
CHECK_THAT(print.wipe_tower_data(1).depth, Catch::Matchers::WithinAbs(0., 1e-6));
|
||||
}
|
||||
|
||||
SECTION("smooth timelapse prints one, and keeps enable_prime_tower on") {
|
||||
DynamicPrintConfig config = tower_estimate_config("rib", 1);
|
||||
config.set_deserialize_strict({ { "timelapse_type", "1" } });
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
REQUIRE(print.has_wipe_tower());
|
||||
CHECK(print.wipe_tower_data(1).depth > 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]")
|
||||
{
|
||||
// Wrapping detection prints a tower on a plate that purges one filament. Neither the old
|
||||
// estimate (which read the wall type and smooth timelapse) nor the old containment gate (the
|
||||
// filament count or smooth timelapse) knew about it, so between them that tower was never
|
||||
// checked against the bed.
|
||||
Print print;
|
||||
Model model;
|
||||
DynamicPrintConfig config = tower_estimate_config("rectangle", 1);
|
||||
// Relative E without a per-layer G92 is rejected before the tower is ever looked at, and
|
||||
// has_wipe_tower() wants a real exclusion polygon before it honours wrapping detection.
|
||||
config.set_deserialize_strict({ { "enable_wrapping_detection", "1" },
|
||||
{ "wrapping_exclude_area", "180x180,190x180,190x190,180x190" },
|
||||
{ "wipe_tower_x", "500" }, { "wipe_tower_y", "500" }, { "use_relative_e_distances", "0" } });
|
||||
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
REQUIRE(print.extruders(true).size() == 1);
|
||||
REQUIRE(print.has_wipe_tower());
|
||||
CHECK(print.wipe_tower_data(1).depth > 0.f);
|
||||
CHECK_THAT(print.validate().string, Catch::Matchers::ContainsSubstring("printable area"));
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_polygon.cpp
|
||||
test_mutable_polygon.cpp
|
||||
test_mutable_priority_queue.cpp
|
||||
test_minimum_spanning_tree.cpp
|
||||
test_nozzle_volume_type.cpp
|
||||
test_step.cpp
|
||||
test_stl.cpp
|
||||
@@ -39,6 +40,8 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_utils.cpp
|
||||
test_timeutils.cpp
|
||||
test_voronoi.cpp
|
||||
test_wipe_tower_estimate.cpp
|
||||
test_wipe_tower.cpp
|
||||
test_optimizers.cpp
|
||||
test_ordering_strategies.cpp
|
||||
# test_png_io.cpp
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "libslic3r/MinimumSpanningTree.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
// A 5x5 lattice: at every step of Prim's algorithm several candidates sit at the same
|
||||
// distance from the tree, so the tie-break decides the tree's shape.
|
||||
static std::vector<Point> lattice()
|
||||
{
|
||||
std::vector<Point> vertices;
|
||||
for (int y = 0; y < 5; ++y)
|
||||
for (int x = 0; x < 5; ++x)
|
||||
vertices.emplace_back(Point::new_scale(x, y));
|
||||
return vertices;
|
||||
}
|
||||
|
||||
static std::vector<Point> sorted_neighbours(const MinimumSpanningTree &mst, const Point &vertex)
|
||||
{
|
||||
std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
|
||||
std::sort(neighbours.begin(), neighbours.end());
|
||||
return neighbours;
|
||||
}
|
||||
|
||||
TEST_CASE("Minimum spanning tree connects every vertex", "[MinimumSpanningTree]")
|
||||
{
|
||||
const std::vector<Point> vertices = lattice();
|
||||
const MinimumSpanningTree mst(vertices);
|
||||
|
||||
REQUIRE(mst.vertices().size() == vertices.size());
|
||||
size_t adjacency_entries = 0;
|
||||
for (const Point &vertex : vertices) {
|
||||
const std::vector<Point> neighbours = mst.adjacent_nodes(vertex);
|
||||
REQUIRE(! neighbours.empty());
|
||||
adjacency_entries += neighbours.size();
|
||||
}
|
||||
// A tree on n vertices has n - 1 edges, each listed from both ends.
|
||||
REQUIRE(adjacency_entries == 2 * (vertices.size() - 1));
|
||||
}
|
||||
|
||||
TEST_CASE("Minimum spanning tree does not depend on the order of the non-root vertices", "[MinimumSpanningTree][Regression]")
|
||||
{
|
||||
const std::vector<Point> vertices = lattice();
|
||||
const MinimumSpanningTree reference(vertices);
|
||||
|
||||
// The root stays first: Prim's tree legitimately depends on where it starts.
|
||||
// Every other order of the remaining vertices must give the same tree.
|
||||
std::vector<std::vector<Point>> orders;
|
||||
orders.emplace_back(vertices);
|
||||
std::reverse(orders.back().begin() + 1, orders.back().end());
|
||||
for (size_t shift = 1; shift + 1 < vertices.size(); ++shift) {
|
||||
orders.emplace_back(vertices);
|
||||
std::rotate(orders.back().begin() + 1, orders.back().begin() + 1 + shift, orders.back().end());
|
||||
}
|
||||
|
||||
for (const std::vector<Point> &order : orders) {
|
||||
const MinimumSpanningTree mst(order);
|
||||
for (const Point &vertex : vertices) {
|
||||
INFO("vertex " << vertex.x() << "," << vertex.y());
|
||||
REQUIRE(sorted_neighbours(mst, vertex) == sorted_neighbours(reference, vertex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/miniz_extension.hpp"
|
||||
|
||||
#include "test_utils.hpp"
|
||||
|
||||
@@ -5104,3 +5106,91 @@ TEST_CASE("Published 3MF denylist and mixed-key sets match the import/export con
|
||||
for (const std::string &key : mixed)
|
||||
CHECK(structural.count(key) == 0);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// data_dir() is a process-wide global that import_presets extracts into; scope it to the test.
|
||||
struct ScopedDataDir
|
||||
{
|
||||
std::string previous = data_dir();
|
||||
explicit ScopedDataDir(const fs::path &dir) { set_data_dir(dir.string()); }
|
||||
~ScopedDataDir() { set_data_dir(previous); }
|
||||
};
|
||||
|
||||
std::string read_file(const fs::path &file)
|
||||
{
|
||||
std::ifstream in(file.string(), std::ios::binary);
|
||||
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
||||
}
|
||||
|
||||
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &entries)
|
||||
{
|
||||
mz_zip_archive zip;
|
||||
mz_zip_zero_struct(&zip);
|
||||
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
||||
for (const auto &[name, content] : entries)
|
||||
REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION));
|
||||
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
||||
REQUIRE(close_zip_writer(&zip));
|
||||
}
|
||||
|
||||
bool any_filename_contains(const fs::path &root, const std::string &needle)
|
||||
{
|
||||
for (fs::recursive_directory_iterator it(root), end; it != end; ++it)
|
||||
if (it->path().filename().string().find(needle) != std::string::npos)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Config import confines zip entries, preset names and bundle ids to the preset directory", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
const fs::path data_root = temp_dir.path() / "datadir";
|
||||
const fs::path src_dir = temp_dir.path() / "src";
|
||||
fs::create_directories(src_dir);
|
||||
ScopedDataDir scoped_data_dir(data_root);
|
||||
|
||||
PresetBundle bundle;
|
||||
AppConfig app_config;
|
||||
const auto confirm = [](std::string const &) { return 1; };
|
||||
const auto import = [&](const fs::path &file) {
|
||||
std::vector<std::string> files{file.string()};
|
||||
bundle.import_presets(files, confirm, ForwardCompatibilitySubstitutionRule::Disable, app_config);
|
||||
return files;
|
||||
};
|
||||
|
||||
const fs::path good_file = src_dir / "Good.json";
|
||||
write_print_preset(bundle.prints.default_preset().config, good_file, "Good");
|
||||
const std::string good_json = read_file(good_file);
|
||||
|
||||
// Four levels up from where import_presets writes (<datadir>/user/default/temp) is temp_dir
|
||||
// itself, so anything that escapes lands where the scan below can see it.
|
||||
const std::string up = "../../../../";
|
||||
const std::string up_win = "..\\..\\..\\..\\";
|
||||
|
||||
SECTION("zip entry names with either separator are reduced to a basename") {
|
||||
const fs::path zip = src_dir / "bundle.zip";
|
||||
write_zip(zip, {{up + "zip-escape.json", "{}"}, {up_win + "zip-escape.json", "{}"}, {"presets/Good.json", good_json}});
|
||||
import(zip);
|
||||
CHECK(bundle.prints.find_preset("Good") != nullptr);
|
||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "zip-escape"));
|
||||
}
|
||||
|
||||
SECTION("a preset name that walks out of the preset directory is rejected") {
|
||||
for (const std::string &name : {up + "name-escape", up_win + "name-escape"}) {
|
||||
const fs::path file = src_dir / "escape.json";
|
||||
write_print_preset(bundle.prints.default_preset().config, file, name);
|
||||
CHECK(import(file).empty());
|
||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "name-escape"));
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("a bundle id that walks out of the bundle directory is rejected") {
|
||||
const fs::path zip = src_dir / "bundle.zip";
|
||||
write_zip(zip, {{BUNDLE_STRUCTURE_JSON_NAME, "{\"id\": \"" + up + "bundle-escape\"}"}, {"Good.json", good_json}});
|
||||
CHECK(import(zip).empty());
|
||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/GCode/WipeTower2.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// A Bambu P1S project that reproduced the off-plate brim: two PLAs priming 30 and 45 mm3 in
|
||||
// separate adhesiveness categories on a 35 mm tower, 0.21 mm layers, 0.4 nozzle (0.5 mm lines),
|
||||
// 150 % infill gap (0.75 mm line pitch), rib width 8, 16 mm tall.
|
||||
static std::vector<WipeTower::PurgeEstimate> cube_purges(int first_category = 100)
|
||||
{
|
||||
return {{30.f, first_category}, {45.f, 0}};
|
||||
}
|
||||
|
||||
TEST_CASE("Cone base polygon bulges past the body box", "[WipeTower]") {
|
||||
// Zero angle: plain body box.
|
||||
const Polygon box = WipeTower2::cone_base_polygon(35., 20., 100., 0.);
|
||||
CHECK(box.points.size() == 4);
|
||||
CHECK(get_extents(box).size() == Point::new_scale(Vec2d(35., 20.)));
|
||||
// A 25-degree cone on a 100 mm tower: base radius R = tan(12.5deg)*100 = 22.2 mm,
|
||||
// which exceeds the body half-depth, so the footprint bulges to center +- R in y
|
||||
// (support_scale keeps the x extent compressed near the body).
|
||||
const Polygon base = WipeTower2::cone_base_polygon(35., 20., 100., 25.);
|
||||
const BoundingBox bb = get_extents(base);
|
||||
const double R = std::tan(25. / 2. * M_PI / 180.) * 100.;
|
||||
CHECK_THAT(unscaled(bb.min.y()), WithinAbs(10. - R, 0.1));
|
||||
CHECK_THAT(unscaled(bb.max.y()), WithinAbs(10. + R, 0.1));
|
||||
// The footprint always contains the body box.
|
||||
CHECK(diff(Polygons{box}, Polygons{base}).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Type1 block-stack depth quantizes each purge to whole lines", "[WipeTower]") {
|
||||
// A 0.5 mm line at 0.21 mm carries 0.0955 mm3 per mm, so across the 34 mm between the
|
||||
// perimeters 30 mm3 is 10 lines and 45 mm3 is 14: 7.5 + 10.5 at the 0.75 mm pitch behind
|
||||
// one perimeter width. The generated mesh of the project measured exactly this.
|
||||
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(cube_purges(), 35.f, 0.21f, 0.4f, 1.5f), WithinAbs(18.5f, 0.01f));
|
||||
// Sharing one category, a layer can never purge into every filament (one of them starts
|
||||
// the layer), so the block is sized by its worst layer and the 10-line purge drops out.
|
||||
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(cube_purges(0), 35.f, 0.21f, 0.4f, 1.5f), WithinAbs(11.0f, 0.01f));
|
||||
CHECK_THAT(WipeTower::estimate_tower_blocks_depth({}, 35.f, 0.2f, 0.4f, 1.f), WithinAbs(0.f, 1e-6f));
|
||||
// A width narrower than two perimeter widths cannot hold purge lines.
|
||||
CHECK_THAT(WipeTower::estimate_tower_blocks_depth({{45.f, 0}}, 0.9f, 0.2f, 0.4f, 1.f), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("A nozzle change adds its ramming lines to the block", "[WipeTower]") {
|
||||
// 10 mm of 1.75 mm filament (24.05 mm3) laid as 1.0 mm nozzle-change lines at 0.2 mm
|
||||
// (0.1914 mm2 each) is 125.7 mm; across the 48.5 mm available that is 3 lines of 1.0 mm.
|
||||
std::vector<WipeTower::PurgeEstimate> purges{{100.f, 0}, {100.f, 0}};
|
||||
const float without_change = WipeTower::estimate_tower_blocks_depth(purges, 50.f, 0.2f, 0.4f, 1.f);
|
||||
purges.front().filament_change_length = 10.f;
|
||||
CHECK_THAT(WipeTower::estimate_tower_blocks_depth(purges, 50.f, 0.2f, 0.4f, 1.f) - without_change, WithinAbs(3.f, 1e-4f));
|
||||
}
|
||||
|
||||
TEST_CASE("Rib tower footprint estimate covers the generated footprint", "[WipeTower]") {
|
||||
// The generated first-layer wall bbox of the project measured 29.56 mm from the sliced
|
||||
// G-code; the volume-only estimate said 23.585 mm.
|
||||
const float side = WipeTower::estimate_rib_tower_bbox_side(cube_purges(), 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 16.f);
|
||||
CHECK(side >= 29.56f);
|
||||
CHECK(side <= 29.56f + 4.f); // without grossly over-reserving plate space
|
||||
// Separate categories stack their blocks, so the footprint must not shrink when they differ.
|
||||
CHECK(side >= WipeTower::estimate_rib_tower_bbox_side(cube_purges(0), 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 16.f));
|
||||
CHECK_THAT(WipeTower::estimate_rib_tower_bbox_side({}, 35.f, 0.2f, 0.4f, 1.f, 8.f, 0.f, 16.f), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("Rib footprint extends the ribs, not the body, below the stability minimum", "[WipeTower]") {
|
||||
// A 10 mm body under a 90 mm print: the ribs stretch to the minimum depth's diagonal, and
|
||||
// the rib width is capped at half the body, so the square grows to minimum + 5 / sqrt(2).
|
||||
const float min_depth = WipeTower::get_limit_depth_by_height(90.f);
|
||||
REQUIRE(min_depth > 10.f);
|
||||
CHECK_THAT(WipeTower::rib_footprint_side(10.f, 10.f, 8.f, 0.f, 90.f), WithinAbs(min_depth + 5.f / std::sqrt(2.f), 1e-4f));
|
||||
// The extra rib length runs along the diagonal, so it shows as its projection on each axis.
|
||||
const float plain = WipeTower::rib_footprint_side(30.f, 30.f, 8.f, 0.f, 5.f);
|
||||
CHECK_THAT(plain, WithinAbs(30.f + 8.f / std::sqrt(2.f), 1e-4f));
|
||||
CHECK_THAT(WipeTower::rib_footprint_side(30.f, 30.f, 8.f, 4.f, 5.f) - plain, WithinAbs(4.f / std::sqrt(2.f), 1e-4f));
|
||||
// A negative extra length cannot pull the ribs inside the diagonal.
|
||||
CHECK_THAT(WipeTower::rib_footprint_side(30.f, 30.f, 8.f, -4.f, 5.f), WithinAbs(plain, 1e-4f));
|
||||
CHECK_THAT(WipeTower::rib_footprint_side(0.f, 30.f, 8.f, 0.f, 5.f), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("Brim width estimate matches each generator's loop quantization", "[WipeTower]") {
|
||||
// 3 mm configured, 0.4 nozzle, 0.2 first layer: 0.4571 mm spacing, 7 loops. WipeTower2
|
||||
// prints and reports the 7 loops; WipeTower reports half a spacing of line width on top.
|
||||
const float spacing = 0.5f - 0.2f * float(1. - M_PI_4);
|
||||
CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, true), WithinAbs(7.f * spacing, 1e-4f));
|
||||
CHECK_THAT(WipeTower::estimate_brim_real_width(3.f, 0.4f, 0.2f, false), WithinAbs(7.5f * spacing, 1e-4f));
|
||||
CHECK_THAT(WipeTower::estimate_brim_real_width(0.f, 0.4f, 0.2f, true), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/GCode/WipeTower.hpp"
|
||||
#include "libslic3r/GCode/WipeTower2.hpp"
|
||||
#include "libslic3r/GCode/WipeTowerEstimate.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
|
||||
// Rectangle wall, one nozzle, 100 mm3 prime volume on a 50 mm wide tower at 0.2 mm layers: one
|
||||
// purge is 10 mm of depth. The flush matrix is off here; the shipped-default case covers it.
|
||||
// Built as PresetBundle::full_config builds the GUI's: apply() creates each enum as a
|
||||
// ConfigOptionEnumGeneric, where full_print_config() would clone the static defaults'
|
||||
// ConfigOptionEnum<T>. The estimate has to read either.
|
||||
static DynamicPrintConfig preset_shaped_defaults()
|
||||
{
|
||||
DynamicPrintConfig config;
|
||||
config.apply(FullPrintConfig::defaults());
|
||||
return config;
|
||||
}
|
||||
|
||||
static DynamicPrintConfig make_config(const char *wall_type = "rectangle")
|
||||
{
|
||||
DynamicPrintConfig config = preset_shaped_defaults();
|
||||
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
|
||||
config.set_key_value("prime_volume", new ConfigOptionFloat(100.));
|
||||
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({100.}));
|
||||
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0}));
|
||||
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(100.));
|
||||
config.set_key_value("wipe_tower_extra_spacing", new ConfigOptionPercent(100.));
|
||||
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(3.));
|
||||
config.set_deserialize_strict("wipe_tower_wall_type", wall_type);
|
||||
config.set_key_value("wipe_tower_rib_width", new ConfigOptionFloat(8.));
|
||||
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
|
||||
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.2));
|
||||
config.set_deserialize_strict("timelapse_type", "0");
|
||||
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||
config.set_key_value("raft_layers", new ConfigOptionInt(0));
|
||||
config.set_key_value("purge_in_prime_tower", new ConfigOptionBool(false));
|
||||
config.set_key_value("single_extruder_multi_material", new ConfigOptionBool(false));
|
||||
return config;
|
||||
}
|
||||
|
||||
static std::vector<unsigned int> filaments(size_t count)
|
||||
{
|
||||
std::vector<unsigned int> ids(count);
|
||||
std::iota(ids.begin(), ids.end(), 0u);
|
||||
return ids;
|
||||
}
|
||||
|
||||
// The first `count` filaments on the given planner; Type2 unless a case says otherwise.
|
||||
static WipeTowerFootprint estimate(const ConfigBase &config, size_t count, double layer_height, double height, WipeTowerType type = WipeTowerType::Type2)
|
||||
{
|
||||
return estimate_wipe_tower_footprint(config, type, filaments(count), layer_height, height);
|
||||
}
|
||||
|
||||
// What both planners print for a 3 mm brim at 0.4 nozzle and 0.2 first layer (0.4571 mm loops).
|
||||
static double printed_brim(double configured, WipeTowerType type)
|
||||
{
|
||||
return WipeTower::estimate_brim_real_width(float(configured), 0.4f, 0.2f, type == WipeTowerType::Type2);
|
||||
}
|
||||
|
||||
TEST_CASE("A rectangle wall tower is sized by the purge volume", "[WipeTowerEstimate]") {
|
||||
const DynamicPrintConfig config = make_config();
|
||||
// Three filaments purge twice per layer; a 5 mm object keeps the stability floor at 5 mm.
|
||||
const WipeTowerFootprint fp = estimate(config, 3, 0.2, 5.);
|
||||
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
|
||||
CHECK_THAT(fp.depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(fp.height, WithinAbs(5., 1e-9));
|
||||
CHECK_THAT(fp.brim_width, WithinAbs(printed_brim(3., WipeTowerType::Type2), 1e-6));
|
||||
// Thinner layers need more depth for the same volume.
|
||||
CHECK_THAT(estimate(config, 3, 0.1, 5.).depth, WithinAbs(40., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Each planner spaces its purge lines by its own option", "[WipeTowerEstimate]") {
|
||||
// Type2 reads wipe_tower_extra_spacing and Type1 prime_tower_infill_gap; neither sees the
|
||||
// other's key. Type2's extra flow cancels out of its depth.
|
||||
DynamicPrintConfig config = make_config();
|
||||
config.set_key_value("wipe_tower_extra_flow", new ConfigOptionPercent(250.));
|
||||
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(20., 1e-9));
|
||||
config.set_key_value("wipe_tower_extra_spacing", new ConfigOptionPercent(150.));
|
||||
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(30., 1e-9));
|
||||
const double type1_spaced = estimate(config, 3, 0.2, 5., WipeTowerType::Type1).depth;
|
||||
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
|
||||
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs(30., 1e-9));
|
||||
// Type1 stacks whole lines behind one 0.5 mm perimeter width, so only the stack scales.
|
||||
CHECK_THAT(estimate(config, 3, 0.2, 5., WipeTowerType::Type1).depth - 0.5, WithinAbs(1.5 * (type1_spaced - 0.5), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("Type1 sizes the tower from each filament's own prime volume", "[WipeTowerEstimate]") {
|
||||
// The Bambu P1S project of the WipeTower cases: 30 and 45 mm3 in two categories on a 35 mm
|
||||
// tower at 0.21 mm, 150 % gap, is 18.5 mm of stacked blocks (11 mm sharing one category).
|
||||
DynamicPrintConfig config = make_config();
|
||||
config.set_key_value("prime_tower_width", new ConfigOptionFloat(35.));
|
||||
config.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
|
||||
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.21));
|
||||
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({30., 45.}));
|
||||
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({100, 0}));
|
||||
const std::vector<WipeTower::PurgeEstimate> purges{{30.f, 100}, {45.f, 0}};
|
||||
const double blocks = WipeTower::estimate_tower_blocks_depth(purges, 35.f, 0.21f, 0.4f, 1.5f);
|
||||
REQUIRE_THAT(blocks, WithinAbs(18.5, 0.01));
|
||||
CHECK_THAT(estimate(config, 2, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(blocks, 1e-4));
|
||||
// The ids pick the volumes, so their order does not matter and a lone filament has no purge.
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, WipeTowerType::Type1, {1, 0}, 0.21, 5.).depth, WithinAbs(blocks, 1e-4));
|
||||
CHECK_THAT(estimate(config, 1, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(0., 1e-9));
|
||||
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0, 0}));
|
||||
CHECK_THAT(estimate(config, 2, 0.21, 5., WipeTowerType::Type1).depth, WithinAbs(11., 0.01));
|
||||
// A rib wall squares the same stack.
|
||||
config.set_deserialize_strict("wipe_tower_wall_type", "rib");
|
||||
const WipeTowerFootprint rib = estimate(config, 2, 0.21, 5., WipeTowerType::Type1);
|
||||
CHECK_THAT(rib.width, WithinAbs(rib.depth, 1e-9));
|
||||
CHECK_THAT(rib.depth, WithinAbs(WipeTower::estimate_rib_tower_bbox_side({{30.f, 0}, {45.f, 0}}, 35.f, 0.21f, 0.4f, 1.5f, 8.f, 0.f, 5.f), 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("A second nozzle adds the ramming of one nozzle change per layer", "[WipeTowerEstimate]") {
|
||||
// Two filaments on two nozzles: the tool order crosses once per layer, and Type1 rams 10 mm
|
||||
// of filament as three 1.0 mm nozzle-change lines (see the WipeTower case).
|
||||
DynamicPrintConfig config = make_config();
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
config.set_key_value("filament_change_length", new ConfigOptionFloats({10., 10.}));
|
||||
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
|
||||
config.set_key_value("filament_map", new ConfigOptionInts({1, 1}));
|
||||
const double same_nozzle = estimate(config, 2, 0.2, 5., WipeTowerType::Type1).depth;
|
||||
config.set_key_value("filament_map", new ConfigOptionInts({1, 2}));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 5., WipeTowerType::Type1).depth - same_nozzle, WithinAbs(3., 1e-4));
|
||||
}
|
||||
|
||||
TEST_CASE("The tower is sized for the first layer when it is the thinnest", "[WipeTowerEstimate]") {
|
||||
// Both planners reserve the worst layer: a 0.28 mm print with a 0.2 mm first layer needs
|
||||
// the 0.2 mm depth, while a thicker first layer changes nothing.
|
||||
DynamicPrintConfig config = make_config();
|
||||
const double at_thinnest = estimate(config, 3, 0.2, 5.).depth;
|
||||
CHECK_THAT(estimate(config, 3, 0.28, 5.).depth, WithinAbs(at_thinnest, 1e-9));
|
||||
config.set_key_value("initial_layer_print_height", new ConfigOptionFloat(0.3));
|
||||
CHECK(estimate(config, 3, 0.28, 5.).depth < at_thinnest);
|
||||
}
|
||||
|
||||
TEST_CASE("Object height sets the stability floor and the auto brim", "[WipeTowerEstimate]") {
|
||||
DynamicPrintConfig config = make_config();
|
||||
// Two filaments purge once: 10 mm, lifted to the 20 mm floor of a 100 mm tower.
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(-1.));
|
||||
const double auto_brim = WipeTower::get_auto_brim_by_height(50.f);
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 50.).brim_width, WithinAbs(printed_brim(auto_brim, WipeTowerType::Type2), 1e-6));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 50., WipeTowerType::Type1).brim_width, WithinAbs(printed_brim(auto_brim, WipeTowerType::Type1), 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A single filament only gets a tower when one is printed anyway", "[WipeTowerEstimate]") {
|
||||
DynamicPrintConfig config = make_config();
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate(config, 0, 0.2, 100.).width, WithinAbs(0., 1e-9));
|
||||
|
||||
// Wrapping detection prints a tower on the first layers whatever the filament count: the
|
||||
// Type1 planner's fixed 10 mm, the stability floor otherwise.
|
||||
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100., WipeTowerType::Type1).depth, WithinAbs(WipeTower::get_wrapping_detection_depth(), 1e-9));
|
||||
config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false));
|
||||
|
||||
// A raft is not one of them: normalize_fdm_2 clears enable_prime_tower for a plate that
|
||||
// purges one filament unless smooth timelapse or wrapping detection is on, so a raft
|
||||
// alone leaves no tower to reserve for.
|
||||
config.set_key_value("raft_layers", new ConfigOptionInt(3));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
|
||||
config.set_key_value("raft_layers", new ConfigOptionInt(0));
|
||||
|
||||
config.set_deserialize_strict("timelapse_type", "1");
|
||||
// A tower printed with no tool change is exactly the planner's idle depth: there is
|
||||
// nothing to purge, and WipeTower2 sizes it at the stability floor.
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 5.).depth, WithinAbs(WipeTower::get_limit_depth_by_height(5.f), 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("A tool change reserves a tower even with nothing to purge", "[WipeTowerEstimate]") {
|
||||
// The purge volumes are configurable down to zero, but the tool changes are still printed
|
||||
// on the tower and both planners still floor it - so the estimate has to floor it too.
|
||||
// Type1 plans per filament and already reserves one; Type2 has only the volume to go on.
|
||||
const double height = GENERATE(5., 100.);
|
||||
const float floor = WipeTower::get_limit_depth_by_height(float(height));
|
||||
const char *wall = GENERATE("rectangle", "rib");
|
||||
DynamicPrintConfig config = make_config(wall);
|
||||
config.set_key_value("prime_volume", new ConfigOptionFloat(0.));
|
||||
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({0.}));
|
||||
|
||||
CHECK(estimate(config, 3, 0.2, height, WipeTowerType::Type2).depth >= floor);
|
||||
CHECK(estimate(config, 3, 0.2, height, WipeTowerType::Type1).depth >= floor);
|
||||
// Still nothing for a lone filament with no other reason.
|
||||
CHECK_THAT(estimate(config, 1, 0.2, height, WipeTowerType::Type2).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, height, WipeTowerType::Type1).depth, WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowerEstimate]") {
|
||||
// A wall type may only change the shape of the tower, never whether one is reserved:
|
||||
// reporting no tower for one that is built collapses the validation hull to a point.
|
||||
const double height = GENERATE(5., 100.);
|
||||
DynamicPrintConfig rect = make_config();
|
||||
DynamicPrintConfig rib = make_config("rib");
|
||||
|
||||
// No tool change and nothing else that prints a tower - neither wall type reserves one.
|
||||
CHECK_THAT(estimate(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
|
||||
// Not even on a dual-nozzle printer, where a lone filament still needs no purge.
|
||||
rect.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
rib.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
CHECK_THAT(estimate(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
|
||||
// With a tool change both reserve one, and both respect the stability floor.
|
||||
CHECK(estimate(rect, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
|
||||
CHECK(estimate(rib, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
|
||||
}
|
||||
|
||||
TEST_CASE("A rib wall squares the tower and caps the rib width", "[WipeTowerEstimate]") {
|
||||
DynamicPrintConfig config = make_config("rib");
|
||||
// sqrt(200 / 0.2) = 31.62 mm square, plus the 8 mm rib bulge along the diagonal.
|
||||
const double body = std::sqrt(1000.);
|
||||
WipeTowerFootprint fp = estimate(config, 3, 0.2, 5.);
|
||||
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + body, 1e-5));
|
||||
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
|
||||
// The extra rib length runs along the diagonal and grows the footprint by its projection.
|
||||
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(4.));
|
||||
CHECK_THAT(estimate(config, 3, 0.2, 5.).depth, WithinAbs((8. + 4.) / std::sqrt(2.) + body, 1e-5));
|
||||
// A tiny tower caps the rib width at half its depth: 5 mm body, 2.5 mm rib.
|
||||
config.set_key_value("wipe_tower_extra_rib_length", new ConfigOptionFloat(0.));
|
||||
config.set_key_value("prime_volume", new ConfigOptionFloat(5.));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs(2.5 / std::sqrt(2.) + 5., 1e-5));
|
||||
}
|
||||
|
||||
TEST_CASE("Every wall and tower type is read the same from a preset and a static config", "[WipeTowerEstimate]") {
|
||||
// The GUI, arrange and the CLI pass a DynamicPrintConfig whose enums are
|
||||
// ConfigOptionEnumGeneric; Print passes a static config whose enums are ConfigOptionEnum<T>.
|
||||
// Both the wall type and the planner selection are read by value, so both give the same shape.
|
||||
const char *wall_type = GENERATE("rectangle", "cone", "rib");
|
||||
const char *tower_type = GENERATE("type1", "type2");
|
||||
DynamicPrintConfig preset = make_config(wall_type);
|
||||
preset.set_deserialize_strict("wipe_tower_type", tower_type);
|
||||
REQUIRE(dynamic_cast<const ConfigOptionEnumGeneric *>(preset.option("wipe_tower_wall_type")) != nullptr);
|
||||
|
||||
FullPrintConfig static_config;
|
||||
static_config.apply(preset, true);
|
||||
REQUIRE(static_config.wipe_tower_wall_type.serialize() == wall_type);
|
||||
REQUIRE(static_config.wipe_tower_type.serialize() == tower_type);
|
||||
|
||||
const WipeTowerType type = resolve_wipe_tower_type(preset);
|
||||
CHECK(type == (std::string(tower_type) == "type1" ? WipeTowerType::Type1 : WipeTowerType::Type2));
|
||||
CHECK(resolve_wipe_tower_type(static_config) == type);
|
||||
|
||||
// Three filaments purge twice per layer on a 5 mm object.
|
||||
const WipeTowerFootprint fp = estimate(preset, 3, 0.2, 5., type);
|
||||
const WipeTowerFootprint from_static = estimate(static_config, 3, 0.2, 5., type);
|
||||
CHECK(fp.depth > 0.);
|
||||
if (std::string(wall_type) == "rib")
|
||||
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
|
||||
else
|
||||
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
|
||||
CHECK_THAT(from_static.width, WithinAbs(fp.width, 1e-9));
|
||||
CHECK_THAT(from_static.depth, WithinAbs(fp.depth, 1e-9));
|
||||
CHECK_THAT(from_static.brim_width, WithinAbs(fp.brim_width, 1e-9));
|
||||
|
||||
// Smooth timelapse is the other enum the estimate reads: a lone filament gets a tower
|
||||
// through both storages too.
|
||||
preset.set_deserialize_strict("timelapse_type", "1");
|
||||
static_config.apply(preset, true);
|
||||
CHECK(estimate(preset, 1, 0.2, 5., type).depth > 0.);
|
||||
CHECK(estimate(static_config, 1, 0.2, 5., type).depth > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("The first-layer outline bulges only for a Type2 cone wall", "[WipeTowerEstimate]") {
|
||||
// Read off a preset-shaped config, whose enums are ConfigOptionEnumGeneric: a cast to
|
||||
// ConfigOptionEnum<T> sees no wall type there and would never find the cone.
|
||||
DynamicPrintConfig config = make_config("cone");
|
||||
config.set_key_value("wipe_tower_cone_angle", new ConfigOptionFloat(25.));
|
||||
REQUIRE(dynamic_cast<const ConfigOptionEnumGeneric *>(config.option("wipe_tower_wall_type")) != nullptr);
|
||||
const Polygon box = Polygon::new_scale({{0., 0.}, {35., 0.}, {35., 20.}, {0., 20.}});
|
||||
auto is_box = [&box](const Polygon &outline) { return diff(Polygons{outline}, Polygons{box}).empty(); };
|
||||
|
||||
// A 25-degree cone on a 100 mm tower has a 22 mm base radius, past the 10 mm half-depth.
|
||||
const Polygon cone = estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type2, 35., 20., 100.);
|
||||
CHECK(unscaled(get_extents(cone).max.y()) > 20. + 1.);
|
||||
CHECK(diff(Polygons{box}, Polygons{cone}).empty());
|
||||
// Type1 ignores the cone option, and the other wall types have no cone.
|
||||
CHECK(is_box(estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type1, 35., 20., 100.)));
|
||||
for (const char *wall_type : {"rectangle", "rib"}) {
|
||||
config.set_deserialize_strict("wipe_tower_wall_type", wall_type);
|
||||
CHECK(is_box(estimate_wipe_tower_first_layer_outline(config, WipeTowerType::Type2, 35., 20., 100.)));
|
||||
}
|
||||
// The static config Print holds gives the same outline.
|
||||
config.set_deserialize_strict("wipe_tower_wall_type", "cone");
|
||||
FullPrintConfig static_config;
|
||||
static_config.apply(config, true);
|
||||
const Polygon from_static = estimate_wipe_tower_first_layer_outline(static_config, WipeTowerType::Type2, 35., 20., 100.);
|
||||
CHECK(from_static.points == cone.points);
|
||||
}
|
||||
|
||||
TEST_CASE("A Bambu Lab printer always gets the Type1 planner", "[WipeTowerEstimate]") {
|
||||
DynamicPrintConfig config = make_config();
|
||||
config.set_deserialize_strict("wipe_tower_type", "type2");
|
||||
config.set_key_value("printer_model", new ConfigOptionString("Bambu Lab X1 Carbon"));
|
||||
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type1);
|
||||
config.set_key_value("printer_model", new ConfigOptionString("Voron 2.4"));
|
||||
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type2);
|
||||
config.erase("wipe_tower_type");
|
||||
CHECK(resolve_wipe_tower_type(config) == WipeTowerType::Type2);
|
||||
}
|
||||
|
||||
TEST_CASE("A dual nozzle purges every filament plus the filament change", "[WipeTowerEstimate]") {
|
||||
DynamicPrintConfig config = make_config();
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4, 0.4}));
|
||||
config.set_key_value("filament_change_length", new ConfigOptionFloats({10., 10.}));
|
||||
config.set_key_value("filament_diameter", new ConfigOptionFloats({1.75, 1.75}));
|
||||
// Two purges of 100 mm3 plus one 10 mm filament change: (200 + 10 * pi * 1.75^2 / 4) / (0.2 * 50).
|
||||
const double change_volume = 10. * PI * 1.75 * 1.75 / 4.;
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs((200. + change_volume) / 10., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTowerEstimate]") {
|
||||
// Both keys default to true, so the shipped configuration purges the flush volumes rather
|
||||
// than the prime volume, with no infill gap on top - the flush volumes already hold it.
|
||||
DynamicPrintConfig config = preset_shaped_defaults();
|
||||
REQUIRE(config.opt_bool("purge_in_prime_tower"));
|
||||
REQUIRE(config.opt_bool("single_extruder_multi_material"));
|
||||
config.set_key_value("prime_tower_width", new ConfigOptionFloat(50.));
|
||||
config.set_deserialize_strict("wipe_tower_wall_type", "rectangle");
|
||||
config.set_key_value("nozzle_diameter", new ConfigOptionFloats({0.4}));
|
||||
|
||||
const double flush_volume = WipeTower2::estimate_semm_flush_volume(config, 2);
|
||||
const double expected = std::max(double(WipeTower::get_limit_depth_by_height(5.f)), flush_volume / (0.2 * 50.));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 5.).depth, WithinAbs(expected, 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A config missing a tower key falls back to that key's default", "[WipeTowerEstimate]") {
|
||||
// The signature takes any ConfigBase: an absent key must read as its declared default.
|
||||
const DynamicPrintConfig full = make_config();
|
||||
DynamicPrintConfig partial = full;
|
||||
partial.erase("wipe_tower_extra_spacing");
|
||||
REQUIRE(partial.option("wipe_tower_extra_spacing") == nullptr);
|
||||
|
||||
DynamicPrintConfig defaulted = full;
|
||||
defaulted.set_key_value("wipe_tower_extra_spacing",
|
||||
print_config_def.get("wipe_tower_extra_spacing")->default_value->clone());
|
||||
CHECK_THAT(estimate(partial, 3, 0.2, 5.).depth, WithinAbs(estimate(defaulted, 3, 0.2, 5.).depth, 1e-9));
|
||||
}
|
||||
Reference in New Issue
Block a user