mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 19:17:33 +00:00
Size the Footprint Estimate from the Planners
The shared estimate reserved every tower with one volume-per-purge rule and the stability floor. Both planners do more: WipeTower (Type1) wipes each filament's own prime volume in whole lines, one block per adhesiveness category sized by its worst layer, rams the leaving filament at every nozzle change, and squares a rib tower from the planned depth; WipeTower2 (Type2) spaces its lines by wipe_tower_extra_spacing, not the Type1-only infill gap, and its extra flow cancels out of the depth. Both extend the ribs rather than the body below the stability minimum, size every layer including a thinner first one, and lay the brim in whole loops, WipeTower reporting half a spacing of line width on top. All of that now lives in estimate_wipe_tower_footprint, fed the planner (resolve_wipe_tower_type mirrors Print::wipe_tower_type and the CLI's Bambu Lab detection) and the filament ids rather than a count. Print passes its own tool set; the PartPlate adapter derives the plate's ids from the passed config and treats an explicit count as a floor, so the CLI's count-only callers size per filament too. The placement clamp also reserves a Type2 cone's base bulge, which the body box does not cover. The planner-mirroring helpers sit beside the planners in WipeTower and WipeTower2 so the two stay in sync; the libslic3r cases pin them to footprints measured from generated G-code.
This commit is contained in:
@@ -41,6 +41,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
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
|
||||
|
||||
93
tests/libslic3r/test_wipe_tower.cpp
Normal file
93
tests/libslic3r/test_wipe_tower.cpp
Normal file
@@ -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));
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
using namespace Slic3r;
|
||||
@@ -28,12 +29,16 @@ 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));
|
||||
@@ -42,51 +47,133 @@ static DynamicPrintConfig make_config(const char *wall_type = "rectangle")
|
||||
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_wipe_tower_footprint(config, 3, 0.2, 5.);
|
||||
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(3., 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_wipe_tower_footprint(config, 3, 0.1, 5.).depth, WithinAbs(40., 1e-9));
|
||||
// The infill gap spaces the purge lines.
|
||||
DynamicPrintConfig spaced = config;
|
||||
spaced.set_key_value("prime_tower_infill_gap", new ConfigOptionPercent(150.));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(spaced, 3, 0.2, 5.).depth, WithinAbs(30., 1e-9));
|
||||
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_wipe_tower_footprint(config, 2, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(estimate(config, 2, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
config.set_key_value("prime_tower_brim_width", new ConfigOptionFloat(-1.));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, 2, 0.2, 50.).brim_width, WithinAbs(WipeTower::get_auto_brim_by_height(50.f), 1e-6));
|
||||
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_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, 0, 0.2, 100.).width, WithinAbs(0., 1e-9));
|
||||
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.
|
||||
// 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_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
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_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(0., 1e-9));
|
||||
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");
|
||||
// Smooth timelapse primes the single filament once: 10 mm, lifted to the floor.
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, 5.).depth, WithinAbs(10., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 100.).depth, WithinAbs(20., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, 5.).depth, WithinAbs(10., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("A tool change reserves the stability floor even with nothing to purge", "[WipeTowerEstimate]") {
|
||||
@@ -97,9 +184,9 @@ TEST_CASE("A tool change reserves the stability floor even with nothing to purge
|
||||
DynamicPrintConfig config = make_config(GENERATE("rectangle", "rib"));
|
||||
config.set_key_value("prime_volume", new ConfigOptionFloat(0.));
|
||||
|
||||
CHECK(estimate_wipe_tower_footprint(config, 3, 0.2, height).depth >= floor);
|
||||
CHECK(estimate(config, 3, 0.2, height).depth >= floor);
|
||||
// Still nothing for a lone filament with no other reason.
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(config, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate(config, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
}
|
||||
|
||||
TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowerEstimate]") {
|
||||
@@ -110,41 +197,40 @@ TEST_CASE("Both wall types agree on whether there is a tower at all", "[WipeTowe
|
||||
DynamicPrintConfig rib = make_config("rib");
|
||||
|
||||
// No tool change and nothing else that prints a tower - neither wall type reserves one.
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
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_wipe_tower_footprint(rect, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(rib, 1, 0.2, height).depth, WithinAbs(0., 1e-9));
|
||||
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_wipe_tower_footprint(rect, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
|
||||
CHECK(estimate_wipe_tower_footprint(rib, 2, 0.2, height).depth >= WipeTower::get_limit_depth_by_height(float(height)));
|
||||
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_wipe_tower_footprint(config, 3, 0.2, 5.);
|
||||
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + body, 1e-9));
|
||||
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 grows the footprint.
|
||||
// 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_wipe_tower_footprint(config, 3, 0.2, 5.).depth, WithinAbs(8. / std::sqrt(2.) + body + 4., 1e-9));
|
||||
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_wipe_tower_footprint(config, 2, 0.2, 5.).depth, WithinAbs(2.5 / std::sqrt(2.) + 5., 1e-9));
|
||||
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>.
|
||||
// The wall type is read by value, so both give the same shape, and the wipe tower
|
||||
// implementation is not an input to the footprint at all.
|
||||
// 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);
|
||||
@@ -156,17 +242,18 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static
|
||||
REQUIRE(static_config.wipe_tower_wall_type.serialize() == wall_type);
|
||||
REQUIRE(static_config.wipe_tower_type.serialize() == tower_type);
|
||||
|
||||
// Three filaments purge twice per layer on a 5 mm object: a 50 x 20 rectangle, or a square.
|
||||
const WipeTowerFootprint fp = estimate_wipe_tower_footprint(preset, 3, 0.2, 5.);
|
||||
if (std::string(wall_type) == "rib") {
|
||||
CHECK_THAT(fp.width, WithinAbs(fp.depth, 1e-9));
|
||||
CHECK_THAT(fp.depth, WithinAbs(8. / std::sqrt(2.) + std::sqrt(1000.), 1e-9));
|
||||
} else {
|
||||
CHECK_THAT(fp.width, WithinAbs(50., 1e-9));
|
||||
CHECK_THAT(fp.depth, WithinAbs(20., 1e-9));
|
||||
}
|
||||
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);
|
||||
|
||||
const WipeTowerFootprint from_static = estimate_wipe_tower_footprint(static_config, 3, 0.2, 5.);
|
||||
// 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));
|
||||
@@ -175,8 +262,19 @@ TEST_CASE("Every wall and tower type is read the same from a preset and a static
|
||||
// through both storages too.
|
||||
preset.set_deserialize_strict("timelapse_type", "1");
|
||||
static_config.apply(preset, true);
|
||||
CHECK(estimate_wipe_tower_footprint(preset, 1, 0.2, 5.).depth > 0.);
|
||||
CHECK(estimate_wipe_tower_footprint(static_config, 1, 0.2, 5.).depth > 0.);
|
||||
CHECK(estimate(preset, 1, 0.2, 5., type).depth > 0.);
|
||||
CHECK(estimate(static_config, 1, 0.2, 5., type).depth > 0.);
|
||||
}
|
||||
|
||||
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]") {
|
||||
@@ -186,7 +284,7 @@ TEST_CASE("A dual nozzle purges every filament plus the filament change", "[Wipe
|
||||
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_wipe_tower_footprint(config, 2, 0.2, 5.).depth, WithinAbs((200. + change_volume) / 10., 1e-9));
|
||||
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]") {
|
||||
@@ -201,19 +299,18 @@ TEST_CASE("The shipped defaults size the tower from the flush matrix", "[WipeTow
|
||||
|
||||
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_wipe_tower_footprint(config, 2, 0.2, 5.).depth, WithinAbs(expected, 1e-6));
|
||||
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("prime_tower_infill_gap");
|
||||
REQUIRE(partial.option("prime_tower_infill_gap") == nullptr);
|
||||
partial.erase("wipe_tower_extra_spacing");
|
||||
REQUIRE(partial.option("wipe_tower_extra_spacing") == nullptr);
|
||||
|
||||
DynamicPrintConfig defaulted = full;
|
||||
defaulted.set_key_value("prime_tower_infill_gap",
|
||||
print_config_def.get("prime_tower_infill_gap")->default_value->clone());
|
||||
CHECK_THAT(estimate_wipe_tower_footprint(partial, 3, 0.2, 5.).depth,
|
||||
WithinAbs(estimate_wipe_tower_footprint(defaulted, 3, 0.2, 5.).depth, 1e-9));
|
||||
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