diff --git a/src/libslic3r/GCode/WipeTowerEstimate.cpp b/src/libslic3r/GCode/WipeTowerEstimate.cpp index d8cfe74527..7f29870b30 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.cpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.cpp @@ -24,6 +24,28 @@ static const ConfigOption *option_of(const ConfigBase &config, const char *key) return nullptr; } +bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament) +{ + const auto *ept = config.option("enable_prime_tower"); + if (ept == nullptr || !ept->value) + return false; + // normalize_fdm_2 only reconsiders the option when the plate uses a filament; below that it + // leaves the user's choice alone, and there is no tower to reason about either way. + if (used_filaments <= 0) + return true; + + const ConfigOption *timelapse = option_of(config, "timelapse_type"); + const bool smooth_timelapse = timelapse != nullptr && timelapse->getInt() == int(TimelapseType::tlSmooth); + const auto *wrapping = config.option("enable_wrapping_detection"); + const bool enable_wrapping = wrapping != nullptr && wrapping->value; + if (smooth_timelapse || enable_wrapping) + return true; + + const ConfigOption *sequence = option_of(config, "print_sequence"); + const bool by_object = sequence != nullptr && sequence->getInt() == int(PrintSequence::ByObject); + return !((used_filaments == 1 && !has_mixed_filament) || (by_object && num_objects > 1)); +} + WipeTowerType resolve_wipe_tower_type(const ConfigBase &config) { // printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag diff --git a/src/libslic3r/GCode/WipeTowerEstimate.hpp b/src/libslic3r/GCode/WipeTowerEstimate.hpp index 387028649b..7cc1e46ecd 100644 --- a/src/libslic3r/GCode/WipeTowerEstimate.hpp +++ b/src/libslic3r/GCode/WipeTowerEstimate.hpp @@ -44,4 +44,17 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase double layer_height, double max_object_height); +// Whether a prime tower is PRINTED for a plate. The footprint estimate above answers how big a +// tower is and reports one for a single filament whenever the flush matrix purges, so it cannot +// answer this; the authority is DynamicPrintConfig::normalize_fdm_2(), which clears +// enable_prime_tower before the plate is sliced. This mirrors that rule so pre-slice consumers can +// ask it without mutating a config, and a test pins the two together. +// +// used_filaments: the plate's filament SLOTS as authored - a mixed slot counts once, matching +// Print::extruders(), which is what normalize_fdm_2 is handed. +// num_objects: objects printed on the plate; only ByObject sequencing looks at it. +// has_mixed_filament: filament_is_mixed is a PROJECT option, so it is passed rather than read - +// `config` only has to carry the print preset's own keys. +bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament); + } // namespace Slic3r diff --git a/src/slic3r/GUI/PartPlate.cpp b/src/slic3r/GUI/PartPlate.cpp index 46e1e1821a..6a494de435 100644 --- a/src/slic3r/GUI/PartPlate.cpp +++ b/src/slic3r/GUI/PartPlate.cpp @@ -1118,27 +1118,28 @@ Polygon PartPlate::imex_wipe_tower_hull() const return Polygon(); const DynamicPrintConfig& print_cfg = preset_bundle->prints.get_edited_preset().config; - const ConfigOptionBool* enable_opt = print_cfg.option("enable_prime_tower"); - if (!enable_opt || !enable_opt->value) + + // Whether a tower is PRINTED is normalize_fdm_2's rule, shared with the slicer through + // prime_tower_is_printed() rather than re-derived here - re-deriving it is what let this + // validate a tower the slicer had already cancelled. Do not substitute the footprint + // estimate: it reports a tower for a single filament whenever the flush matrix purges + // (SEMM + purge_in_prime_tower, the Klipper default), which hard-blocks a plate with + // nothing drawn on screen to move. + // + // Counts are the ones normalize_fdm_2 is handed: filament slots as authored, so a mixed + // slot counts once, and distinct objects rather than instances. filament_is_mixed is a + // project option, hence read separately. + const auto* mixed_opt = preset_bundle->project_config.option("filament_is_mixed"); + const bool has_mixed = mixed_opt != nullptr && has_any_mixed_filament(mixed_opt->values); + std::set plate_objects; + for (const auto& pr : obj_to_instance_set) + plate_objects.insert(pr.first); + if (!prime_tower_is_printed(print_cfg, (int) get_extruders(true, /*expand_mixed=*/false).size(), + (int) plate_objects.size(), has_mixed)) return Polygon(); - // Approximates normalize_fdm_2(), which is what actually decides whether a tower is - // printed: it clears enable_prime_tower for one filament, or ByObject over several - // objects, unless smooth timelapse or wrapping detection forces one. - // - // Do not substitute the footprint estimate's answer. It reports a tower for a single - // filament whenever the flush matrix purges (SEMM + purge_in_prime_tower, the Klipper - // default), which would hard-block a plate whose tower normalize_fdm_2 already cleared - - // with nothing drawn on screen to move. See test_wipe_tower_estimate.cpp. - auto timelapse_type = print_cfg.option>("timelapse_type"); - bool need_wipe_tower = timelapse_type ? (timelapse_type->value == TimelapseType::tlSmooth) : false; - // enable_wrapping_detection is a PRINT option; read from the printer preset it returns - // nullptr and silently skips validation for a tower the user can see and drag. - if (auto wrapping_opt = print_cfg.option("enable_wrapping_detection")) - need_wipe_tower |= wrapping_opt->value; + // The estimate's floor counts what is purged, so a mixed slot counts as its components here. const int plate_extruder_size = (int) get_extruders(true).size(); - if (!need_wipe_tower && plate_extruder_size < 2) - return Polygon(); Vec3d wt_pos, wt_size; // full_config(), not the print preset: wipe_tower_x/y are project options the estimate diff --git a/tests/libslic3r/test_wipe_tower_estimate.cpp b/tests/libslic3r/test_wipe_tower_estimate.cpp index 6356d6f452..7a4f874971 100644 --- a/tests/libslic3r/test_wipe_tower_estimate.cpp +++ b/tests/libslic3r/test_wipe_tower_estimate.cpp @@ -6,6 +6,7 @@ #include "libslic3r/GCode/WipeTower2.hpp" #include "libslic3r/GCode/WipeTowerEstimate.hpp" #include "libslic3r/PrintConfig.hpp" +#include "libslic3r/FilamentMixer.hpp" #include #include @@ -381,3 +382,41 @@ TEST_CASE("A config missing a tower key falls back to that key's default", "[Wip 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)); } + +TEST_CASE("prime_tower_is_printed answers exactly what normalize_fdm_2 decides", "[WipeTowerEstimate]") { + // The estimate says how big a tower is; this says whether there is one, and the authority is + // normalize_fdm_2, which clears enable_prime_tower before the plate is sliced. Pre-slice + // consumers cannot call it (it mutates a config), so the rule is mirrored - and mirrored rules + // drift, which is what this pins. Every combination the rule looks at, both verdicts compared. + const int used_filaments = GENERATE(1, 2, 3); + const bool has_mixed = GENERATE(false, true); + const int num_objects = GENERATE(1, 2); + const bool by_object = GENERATE(false, true); + const bool smooth = GENERATE(false, true); + const bool wrapping = GENERATE(false, true); + + DynamicPrintConfig config = preset_shaped_defaults(); + config.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); + config.set_deserialize_strict("print_sequence", by_object ? "by object" : "by layer"); + config.set_deserialize_strict("timelapse_type", smooth ? "1" : "0"); + config.set_key_value("enable_wrapping_detection", new ConfigOptionBool(wrapping)); + config.set_key_value("filament_is_mixed", new ConfigOptionBools( + has_mixed ? std::vector{0, 1} : std::vector{0, 0})); + REQUIRE(has_any_mixed_filament(config.option("filament_is_mixed")->values) == has_mixed); + + DynamicPrintConfig normalized = config; + normalized.normalize_fdm_2(num_objects, used_filaments); + const bool slicer_prints_one = normalized.opt_bool("enable_prime_tower"); + + CHECK(prime_tower_is_printed(config, used_filaments, num_objects, has_mixed) == slicer_prints_one); +} + +TEST_CASE("prime_tower_is_printed follows the option the user set", "[WipeTowerEstimate]") { + DynamicPrintConfig config = preset_shaped_defaults(); + config.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); + CHECK_FALSE(prime_tower_is_printed(config, 2, 1, false)); + + // Below one filament normalize_fdm_2 leaves the option alone, so this does too. + config.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); + CHECK(prime_tower_is_printed(config, 0, 1, false)); +}