Compare commits

...
Author SHA1 Message Date
Hanif Koh c9d06a1372 Size the Prime Tower Estimate from a Mixed Slot's Physical Components
Print::validate sizes the pre-slice prime tower from Print::extruders(),
which lists every mixed filament slot under its own id, so the estimator
planned one purge block per slot. The tool ordering resolves a mixed slot
to one physical component on every layer, with or without colour
sublayers, and the generated tower only purges those. The GUI preview and
its placement clamp already expand the slots, so the tower was drawn
inside the bed and then refused as partially outside it: with 8 mixed
slots at 0.08 mm layers the estimate was 114 mm for a tower that prints
at 64 mm.

The estimator now expands mixed slots to their components itself, so
validation, the GUI and the CLI placement size the same tower.
2026-10-13 19:39:26 +08:00
3 changed files with 35 additions and 1 deletions
+11 -1
View File
@@ -4,6 +4,7 @@
#include "WipeTower.hpp"
#include "WipeTower2.hpp"
#include "../Config.hpp"
#include "../FilamentMixer.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
@@ -74,10 +75,19 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
}
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_slots, double layer_height, double max_object_height)
{
WipeTowerFootprint footprint;
footprint.height = max_object_height;
// A mixed slot is virtual: the tool ordering resolves it to one of its physical components
// on every layer, with or without colour sublayers, so the tower only ever purges those.
// Validation passes the slots as Print::extruders() lists them; the GUI and CLI placement
// expand them beforehand, which repeating here leaves unchanged.
std::vector<unsigned int> filament_ids = filament_slots;
const auto *is_mixed_opt = dynamic_cast<const ConfigOptionBools *>(option_of(config, "filament_is_mixed"));
const auto *components_opt = dynamic_cast<const ConfigOptionStrings *>(option_of(config, "filament_mixed_components"));
if (is_mixed_opt != nullptr && components_opt != nullptr && has_any_mixed_filament(is_mixed_opt->values))
filament_ids = expand_mixed_filaments(filament_slots, is_mixed_opt->values, components_opt->values);
const size_t filaments_cnt = filament_ids.size();
if (filaments_cnt == 0 || layer_height < EPSILON)
return footprint;
@@ -34,6 +34,8 @@ Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTo
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
// changes, so ids derived from the model must include them
// (Print::extruders(true)) or a real tower is sized as if it were never built.
// A mixed slot may be passed as itself or already expanded: it is sized as its
// physical components either way, since those are all the tower ever purges.
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
//
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
@@ -133,6 +133,28 @@ TEST_CASE("Type1 sizes the tower from each filament's own prime volume", "[WipeT
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 mixed slot is purged as its physical components", "[WipeTowerEstimate]") {
// Slots 4 and 5 blend 1+2 and 1+3: every layer prints one component per mixed slot, so a
// plate using all five slots purges the three physical filaments and nothing more.
const WipeTowerType type = GENERATE(WipeTowerType::Type1, WipeTowerType::Type2);
DynamicPrintConfig config = make_config();
config.set_key_value("filament_prime_volume", new ConfigOptionFloats({100., 100., 100., 100., 100.}));
config.set_key_value("filament_adhesiveness_category", new ConfigOptionInts({0, 0, 0, 0, 0}));
config.set_key_value("filament_is_mixed", new ConfigOptionBools(std::vector<unsigned char>{0, 0, 0, 1, 1}));
config.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "", "1,2", "1,3"}));
const double physical = estimate(config, 3, 0.2, 5., type).depth;
REQUIRE(physical > estimate(config, 2, 0.2, 5., type).depth);
CHECK_THAT(estimate(config, 5, 0.2, 5., type).depth, WithinAbs(physical, 1e-9));
// The same slots with the mixed flags cleared are five filaments, which is what sized the
// tower before and refused positions the printed tower fits.
DynamicPrintConfig unmixed = config;
unmixed.set_key_value("filament_is_mixed", new ConfigOptionBools(std::vector<unsigned char>{0, 0, 0, 0, 0}));
CHECK(estimate(unmixed, 5, 0.2, 5., type).depth > physical);
// Mixed slots alone still purge their components, and a single one already changes tools.
CHECK_THAT(estimate_wipe_tower_footprint(config, type, {3, 4}, 0.2, 5.).depth, WithinAbs(physical, 1e-9));
CHECK_THAT(estimate_wipe_tower_footprint(config, type, {3}, 0.2, 5.).depth, WithinAbs(estimate(config, 2, 0.2, 5., type).depth, 1e-9));
}
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).